window.AScript = new Map(); window.isTouchAvailable = 'ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0 window.getPrimaryPointerEvent = function () { const isIOS = /iP(ad|hone|od)/.test(navigator.userAgent); const supportsPointer = !!window.PointerEvent; if (supportsPointer && !isIOS) { return "pointerdown"; // chuẩn nhất } if ( 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0 ) { return "touchstart"; // fallback tốt nhất cho iOS hoặc thiết bị cũ } return "mousedown"; // fallback cho desktop không cảm ứng }; window.isValidPointerClick = function (ev) { return ev.pointerType === 'mouse' && ev.button !== 0; } if (Node.prototype.appendChildren === undefined) { Node.prototype.appendChildren = function () { let children = [...arguments]; if ( children.length == 1 && Object.prototype.toString.call(children[0]) === "[object Array]" ) { children = children[0]; } var documentFragment = document.createDocumentFragment(); children.forEach(c => documentFragment.appendChild(c)); this.appendChild(documentFragment); }; } if (Node.prototype.removeAll === undefined) { Node.prototype.removeAll = function () { while (this.firstChild) this.removeChild(this.lastChild); }; } if (NodeList.prototype.removeAll === undefined) { NodeList.prototype.removeAll = function () { for (var i = this.length - 1; i >= 0; i--) { this[i].remove(); } }; } if (Array.prototype.hasItem === undefined) { Array.prototype.hasItem = function (o, callback) { var f = false; this.forEach(e => { if (callback(e, o)) { f = true; return; } }); return f; } } if (Array.prototype.removeItem === undefined) { Array.prototype.removeItem = function (o, callback) { var f = false; this.forEach((e, i) => { if (callback(e, o)) { delete this[i]; this.splice(i, 1); } }); return f; } } window.getOS = function () { var userAgent = window.navigator.userAgent, platform = window.navigator.platform, macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'], iosPlatforms = ['iPhone', 'iPad', 'iPod'], os = null; if (macosPlatforms.indexOf(platform) !== -1) { os = 'Mac OS'; } else if (iosPlatforms.indexOf(platform) !== -1) { os = 'iOS'; } else if (windowsPlatforms.indexOf(platform) !== -1) { os = 'Windows'; } else if (/Android/.test(userAgent)) { os = 'Android'; } else if (!os && /Linux/.test(platform)) { os = 'Linux'; } return os; } window.GetAbsoluteURL = function(relativeURL) { if (relativeURL.startsWith(window.location.origin)) { return relativeURL; } return window.location.origin + relativeURL; } window.getPathFromUrl = function(url) { const parsed = new URL(url); return parsed.pathname + parsed.search + parsed.hash; } window.GetEventType = function () { if (isTouchAvailable) { return "touchend"; } else { return "click"; } } window.fireEvent = function (element, event) { if (document.createEventObject) { // dispatch for IE var evt = document.createEventObject(); return element.fireEvent('on' + event, evt) } else { // dispatch for firefox + others var evt = document.createEvent("HTMLEvents"); evt.initEvent(event, true, true); // event type,bubbling,cancelable return !element.dispatchEvent(evt); } } window.requestTimeout = function (fn, delay, registerCancel = () => { }) { const start = new Date().getTime(); const loop = () => { const delta = new Date().getTime() - start; if (delta >= delay) { fn(); registerCancel(function () { }); return; } const raf = requestAnimationFrame(loop); registerCancel(() => cancelAnimationFrame(raf)); }; const raf = requestAnimationFrame(loop); registerCancel(() => cancelAnimationFrame(raf)); }; window.formatDateToString = function (date) { var dd = (date.getDay() < 10 ? '0' : '') + date.getDay(); var MM = ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1); var yyyy = date.getFullYear(); var hh = (date.getHours() < 10 ? '0' : '') + date.getHours(); var mm = (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); var ss = (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); return (dd + "-" + MM + "-" + yyyy + " " + hh + ":" + mm + ":" + ss); } window.padLeadingZeros = function (num, size) { var s = num + ""; while (s.length < size) s = "0" + s; return s; } window.AddFormData = function (frm, name, val) { if (frm.has(name)) { frm.set(name, val); v } else { frm.append(name, val); } } window.checkViewHeight = function () { let vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); } window.checkViewHeight(); window.addEventListener('resize', () => { window.checkViewHeight(); }); window.LongIdGenerator = class { constructor(start = 1n) { this.counter = BigInt(start); } next() { return this.counter++; } reset(start = 1n) { this.counter = BigInt(start); } } window.AObject = class { constructor() { this.listeners = new Map(); // label -> Map this.onceListeners = new Map(); this.triggerdLabels = new Map(); this.listenersById = new Map(); // id -> { label, callback } this.systemEvents = new Map(); this.customEvents = new Map(); this.parentEventMap = new Map(); this.listAObject = new Set(); this.eventName = window.getPrimaryPointerEvent(); this.isScrolling = false; } _fCheckPast(label, callback) { if (this.triggerdLabels.has(label)) { callback(this.triggerdLabels.get(label)); return true; } else { return false; } } on(label, callback, checkPast = false) { const id = crypto.randomUUID(); const cb = (...args) => callback(...args); if (!this.listeners.has(label)) { this.listeners.set(label, new Map()); } this.listeners.get(label).set(id, cb); this.listenersById.set(id, { label, callback: cb }); if (checkPast) this._fCheckPast(label, callback); return id; } onReady(label, callback) { return this.on(label, callback, true); } once(label, callback, checkPast = false) { const id = crypto.randomUUID(); const cb = (...args) => callback(...args); if (checkPast && this._fCheckPast(label, callback)) return; if (!this.onceListeners.has(label)) { this.onceListeners.set(label, new Map()); } this.onceListeners.get(label).set(id, cb); this.listenersById.set(id, { label, callback: cb }); return id; } onceReady(label, callback) { return this.once(label, callback, true); } off(label, callback = true) { if (callback === true) { this.listeners.delete(label); this.onceListeners.delete(label); } else { const removeMatching = (map) => { const innerMap = map.get(label); if (innerMap) { for (let [id, cb] of innerMap.entries()) { if (cb === callback) { innerMap.delete(id); this.listenersById.delete(id); } } if (innerMap.size === 0) map.delete(label); } }; removeMatching(this.listeners); removeMatching(this.onceListeners); } } offById(id) { const info = this.listenersById.get(id); if (info) { const { label, callback } = info; const removeFrom = (map) => { const innerMap = map.get(label); if (innerMap && innerMap.has(id)) { innerMap.delete(id); if (innerMap.size === 0) map.delete(label); } }; removeFrom(this.listeners); removeFrom(this.onceListeners); this.listenersById.delete(id); } } trigger(label, ...args) { let res = false; this.triggerdLabels.set(label, ...args); const triggerFrom = (map) => { const innerMap = map.get(label); if (innerMap) { for (let cb of innerMap.values()) { cb(...args); res = true; } } }; triggerFrom(this.onceListeners); triggerFrom(this.listeners); this.onceListeners.delete(label); return res; } dispose() { for (const [element, eventMap] of this.systemEvents.entries()) { for (const [event, { callback }] of eventMap.entries()) { element.removeEventListener(event, callback, false); } } for (const [id, obj] of this.customEvents.entries()) { obj.ele.offById(id); } for (const id of this.listAObject) { id.dispose(); } this.listAObject.clear(); this.systemEvents.clear(); this.parentEventMap.clear(); // Hủy các custom event listeners this.listeners.clear(); this.onceListeners.clear(); this.listenersById.clear(); this.triggerdLabels.clear(); } removeSystemEvent(element) { const eventMap = this.systemEvents.get(element); if (!eventMap) return; for (const [event, { callback, parent }] of eventMap.entries()) { element.removeEventListener(event, callback, false); // Nếu có parent, thì xóa tham chiếu khỏi parentEventMap if (parent && this.parentEventMap.has(parent)) { const entrySet = this.parentEventMap.get(parent); for (let item of entrySet) { if (item.element === element && item.event === event) { entrySet.delete(item); break; } } if (entrySet.size === 0) { this.parentEventMap.delete(parent); } } } this.systemEvents.delete(element); } removeSytemEventParent(parent) { const entries = this.parentEventMap.get(parent); if (!entries) return; let iC = 0; for (const { element, eventName } of entries) { const eventMap = this.systemEvents.get(element); if (eventMap) { const data = eventMap.get(eventName); if (data && typeof data.callback === 'function') { element.removeEventListener(eventName , data.callback, false); eventMap.delete(eventName); // Nếu element không còn event nào, xóa khỏi systemEvents if (eventMap.size === 0) { this.systemEvents.delete(element); } } } } this.parentEventMap.delete(parent); } removeCustomEventParent(parent) { const entries = this.parentEventMap.get("Custom" + parent); if (!entries) return; for (const { id, element } of entries) { const eventMap = this.customEvents.get(id); if (eventMap) { eventMap.ele.offById(eventMap.id); this.customEvents.delete(id); } } this.parentEventMap.delete(parent); } addCustomEvent(id, element, groups = null) { if (!this.customEvents.has(id)) { this.customEvents.set(id, { "ele":element, "groups":groups }); } if (groups) { if (!this.parentEventMap.has("Custom" + parent)) { this.parentEventMap.set("Custom" + parent, new Map()); } this.parentEventMap.get("Custom" + parent).set(id, element); } } addSystemEvent(eventName, element, callback, parent = null, capture = false) { if (!this.systemEvents.has(element)) { this.systemEvents.set(element, new Map()); } const eventMap = this.systemEvents.get(element); eventMap.set(eventName, { callback, parent }); element.addEventListener(eventName, callback, capture); if (parent) { if (!this.parentEventMap.has(parent)) { this.parentEventMap.set(parent, new Set()); } this.parentEventMap.get(parent).add({ element, eventName }); } } removeAllChildNodes(parent) { while (parent.firstChild) { parent.removeChild(parent.firstChild); } } cloneNodes(source, destination) { const children = source.childNodes; for (let i = 0; i < children.length; i++) { destination.appendChild(children[i].cloneNode(true)); } } }; class LoadScriptAsync extends window.AObject { constructor(type) { super(); this.id = 0; this.type = type; this.listCurrent = new Map(); this.stackScript = []; this.jsLib = document.querySelector("section[app-js-lib]"); this.jsPage = document.querySelector("section[app-js-page]"); this.count = 0; } getScripts() { document.querySelectorAll("section [js-lib]").forEach(el => { if (!el.hasAttribute("checked")) { this.listCurrent.set(el.src.split(/(\\|\/)/g).pop(), { "el": el.src, "status": "0" }); el.setAttribute("checked", ""); } }); } processScript(doc) { if (typeof doc.childNodes === "undefined") { return; } this.stackScript = []; this.listCurrent = new Map(); this.getScripts(); for (var i = 0; i < doc.childNodes.length; i++) { var n = doc.childNodes[i]; if (n.getAttribute("js-page") != null) { this.stackScript.push({ "el": n }); } if (n.getAttribute("js-lib") != null) { if (this.checkExist(n)) { continue; } var src = n.getAttribute("src"); this.jsLib.appendChild(this.createScriptTag(n)); this.listCurrent.set(src.split(/(\\|\/)/g).pop(), { "el": src, "status": "0" }); } } window.requestTimeout(this.checkLoaded.bind(this), 10, window.registerCancel); } checkLoaded() { this.listCurrent.forEach((v, k, m) => { var tn = k.substring(0, k.length - 3); if (window.AScript.has(tn)) { this.listCurrent.delete(k); } }); if (this.listCurrent.size == 0) { this.addJsPage(); this.trigger("Loaded", null); } else { window.requestTimeout(this.checkLoaded.bind(this), 10, window.registerCancel); } } checkExist(elm) { if (this.listCurrent.has(elm.src.split(/(\\|\/)/g).pop())) { return true; } else { return false; } } createScriptTag(el) { var newScript = document.createElement("script"); newScript.setAttribute("async", ""); for (var i = 0; i < el.attributes.length; i++) { newScript.setAttribute(el.attributes[i].name, el.attributes[i].value); } if (!el.hasChildNodes()) { newScript.src = el.src; } else { var tmp = document.createTextNode(el.innerHTML); newScript.appendChild(tmp); } return newScript; } addJsPage() { this.jsPage.innerHTML = ""; if (window.Destroy != undefined) window.Destroy(); this.stackScript.forEach(el => { this.jsPage.appendChild(this.createScriptTag(el.el)); }); } } class AApp extends window.AObject { constructor(container = '[app-content]') { super(); this.cachePage = new CacheManager(); this.isLoadedLayout = false; window.Destroy = undefined; window.ALayout = new Map(); this.isRedirectPage = false; this.metaPage = document.head.querySelector("meta[name=idPage]"); this.pageName = this.metaPage.getAttribute("pageName"); this.lName = this.metaPage.getAttribute("layName"); var tmp = document.querySelector(container); this.mainApp = tmp.querySelector("[main-content]") ? tmp.querySelector("[main-content]") : tmp; var f = function (ev) { if (ev.state) { this.cachePage.get(ev.state.url, ((result) => { if (result == null) { } else { this.loadContentPage(result.html); this.metaPage.content = result.idPage; this.metaPage.setAttribute("isLayout", result.isLayout); this.trigger("redirect_page", ev.state); this.checkLayout(result.lName); var l = new LoadScriptAsync("Page"); l.processScript(result.doc); l.on("Loaded", (() => { this.loadedPage(); }).bind(this)); } }).bind(this)); } }.bind(this); window.addEventListener("popstate", f); } checkLayout(lName) { if (this.lName !== lName) { if (this.lName !== "None") { const l = window.ALayout.get(this.lName); l.dispose(); } this.callLoadLayout(lName); this.lName = lName; } } render() { if (this.lName !== "None") { this.callLoadLayout(this.lName); } else { this.isLoadedLayout = true; } this.renderNoLayout(); } addAttributeURL(url, str) { if (url.indexOf("?") != -1) { return url + "&" + str.replace("?", ""); } else { return url + str; } } renderNoLayout() { (function () { this.callLoadPage(window.location.href); }).bind(this)(); } scrollTop() { return window.scrollY || window.smScroll.scrollTop; } checkVisible(elm) { var rect = elm.getBoundingClientRect(); var viewHeight = window.innerHeight; return !(rect.bottom < 0 || rect.top - viewHeight >= 0); } initScrollBar() { if (window.getOS() == "iOS") { document.querySelector(".main-scrollbar[data-scrollbar]").classList.add("iOS"); let scrollY = 0; let ticking = false; window.addEventListener('scroll', ((event) => { scrollY = window.scrollY; if (!ticking) { window.requestAnimationFrame(() => { this.trigger("App_Scrolling", scrollY); ticking = false; }); ticking = true; } }).bind(this)); } else { window.Scrollbar.use(window.OverscrollPlugin); var sOption = { damping: (window.getOS() == "Android") ? .08 : .04, thumbMinSize: 25, renderByPixel: true, alwaysShowTracks: true, continuousScrolling: true, plugins: { overscroll: { effect: 'bounce', damping: .15,//0.15 maxOverscroll: 250 } } }; window.smScroll = window.Scrollbar.init(document.querySelector('.main-scrollbar[data-scrollbar]'), sOption); window.smScroll.addListener((status => { this.trigger("App_Scrolling", status.offset.y); }).bind(this)); } } initNavs(groups = "Defaul") { document.querySelectorAll("[app-nav]").forEach(((el) => { if (el.getAttribute("app-nav") !== 'true') { const f1 = function (ev) { ev.preventDefault(); }; this.addSystemEvent("click", el, f1, groups) const f2 = (function (ev) { if (window.isValidPointerClick(ev)) return; this.initNavApp(ev.currentTarget.getAttribute("href"), ev.currentTarget.hasAttribute("isflexpage")); }).bind(this); this.addSystemEvent(this.eventName, el, f2, groups); el.setAttribute("app-nav", true); } }).bind(this)); } initNavApp(t, flex = false) { if (window.getOS() == "iOS") { window.scrollTo({ top: 0, behavior: 'smooth' }); } else { window.smScroll.scrollTo(0, 0, 500); } this.isRedirectPage = true; this.callLoadPage(window.GetAbsoluteURL(t), flex); } callLoadPage(url, flex = false) { url = window.getPathFromUrl(url); this.cachePage.searchFlexPage(url, ((result) => { if (result) { this.cachePage.get(result.layout.linkID, ((result1) => { if (result1) { const a = result.layout.info; const doc2 = new DOMParser().parseFromString(result1.doc, "text/html"); const doc = doc2.firstChild.querySelector("head"); const obj = { html: result1.html, title: a.title, idPage: a.idPage, lName: a.lName, doc: doc, dynamicF: a.dynamicF }; this.setContentPage(obj, url); } else { this.getPage(url, result) } }).bind(this), "flex"); } else { this.getPage(url); } }).bind(this)); //if (flex) { //} else { // this.cachePage.get(url, ((result) => { // if (result) { // // Set content page từ cache // } else { // this.getPage(url); // Load mới nếu chưa có trong cache // } // }).bind(this)); //} } loadContentPage(content) { const tpl = document.createElement('template'); tpl.innerHTML = content; for (var i = this.mainApp.childNodes.length - 1; i >= 0; i--) { this.mainApp.childNodes[i].remove(); } this.mainApp.appendChild(tpl.content); } contentPage(page, url) { document.title = page.title + " - " + this.pageName; var meta = document.head.querySelector("meta[name=idPage]"); document.body.setAttribute("page", page.idPage); meta.content = page.idPage; meta.setAttribute("layName", page.lName); this.loadContentPage(page.html); window.history.pushState({"url":url}, page.title, url); } setContentPage(page, url) { this.contentPage(page, url); var l = new LoadScriptAsync("Page"); if (this.isRedirectPage) { this.trigger("redirect_page", page); this.checkLayout(page.lName); this.isRedirectPage = false; } l.on("Loaded", (() => { this.loadedPage(); }).bind(this)); l.processScript(page.doc); } getPage(url, rs = null) { var xhr = new XMLHttpRequest(); xhr.open("GET", this.addAttributeURL(url, "?vr=cAsync")); xhr.send(); var f = function (evt) { if (evt.currentTarget.readyState == 4 && evt.currentTarget.status == 200) { if (evt.currentTarget.responseText) { var jP = JSON.parse(evt.currentTarget.responseText); this.loadPage(jP, window.GetAbsoluteURL(url), rs); } } }.bind(this); xhr.addEventListener("readystatechange", f, false); } loadPage(o, url, rs) { const title = o.Title; const idPage = o.PageId; const doc2 = new DOMParser().parseFromString(o.Scripts, "text/html"); const doc = doc2.firstChild.querySelector("head"); const dF = doc.querySelector("script[dynamic]"); const obj = { html: o.Content, title: title, idPage: idPage, lName: o.LayoutName, flexPageID: o.FlexPageId, doc: doc, dynamicF: dF ? dF.getAttribute("dynamic") : null }; this.cachePage.set(url, obj); this.setContentPage(obj, url); } loadedPage() { this.metaPage = document.head.querySelector("meta[name=idPage]"); if (this.metaPage != null && this.isLoadedLayout) { console.log(window.location.href); this.cachePage.searchFlexPage(window.getPathFromUrl(window.location.href), ((tmp) => { tmp = tmp.layout.info; if (tmp != null && tmp.dynamicF != null) { if (window[tmp.dynamicF] != null) window[tmp.dynamicF](); } else { if (window["L" + this.metaPage.content] != null) window["L" + this.metaPage.content](); } this.initNavs(this.metaPage.content); this.trigger("pageLoaded", null); }).bind(this)); } else { window.requestTimeout(this.loadedPage.bind(this), 10, window.registerCancel); } } callLoadLayout(lName) { this.cachePage.get("layout|" + lName, ((result) => { if (result) { this.setLayout(result); // Set content page từ cache } else { console.log("connect new Layout"); this.loadLayout(); // Load mới nếu chưa có trong cache } }).bind(this), "layout"); } loadLayout() { (function () { var xhr = new XMLHttpRequest(); xhr.open("GET", this.addAttributeURL(window.location.href, "?vr=lAsync")); xhr.send(); var f = function (evt) { if (evt.currentTarget.readyState == 4 && evt.currentTarget.status == 200) { if (evt.currentTarget.responseText) { var jP = JSON.parse(evt.currentTarget.responseText); this.setLayout(jP); this.cachePage.set(this.lName, jP, "layout"); } } }.bind(this); xhr.addEventListener("readystatechange", f, false); }).bind(this)(); } loadedLayout() { if (this.lName !== "None") { const l = window.ALayout.get(this.lName); if (!l.isLoaded) { l.renderMenu(); this.isLoadedLayout = true; this.trigger("layoutLoaded", null); } } } setLayout(o) { var oP = new DOMParser(); var pHtml = oP.parseFromString(o.Content, 'text/html'); (function () { pHtml.body.childNodes.forEach(function (item) { var t = document.getElementById(item.getAttribute("id")); if (t) { item.classList.forEach(el => { t.classList.add(el); }); item.childNodes.forEach(el => { t.appendChild(el.cloneNode(true)); }); } }); }).bind(this)(); (function () { var doc = oP.parseFromString(o.Scripts, 'text/html').head; var l = new LoadScriptAsync("Layout"); l.on("Loaded", (() => { this.loadedLayout(); }).bind(this)); l.processScript(doc); }).bind(this)(); } loadCSS(arr) { for (var i = 0; i < arr.length; i++) { var link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('href', arr[i]); document.head.appendChild(link); } } } class CacheManager { constructor() { this.storage = new CacheStorage("appCache"); this.pageMap = new Map(); this.layoutMap = new Map(); this.readyCallbacks = []; this.isReady = false; this.flexPages = new FlexPageTrie(); this.countFP = 0; this._loadIndex(); window.addEventListener("beforeunload", (function (e) { this.storage.set("treePage", this.flexPages.toJSON(), function () { }); }).bind(this)); } _key(id, type = "page") { return `${type}|${id}`; } _onReady(callback) { if (this.isReady) { callback(); } else { this.readyCallbacks.push(callback); } } _loadIndex() { this.storage.getAllKeys(((keys) => { let pending = 0; keys.forEach(((key) => { if (!key.startsWith(`${this.storage.prefix}|`)) return; const shortKey = key.replace(`${this.storage.prefix}|`, ""); const [type, id] = shortKey.split("|"); if (type === "treePage") { pending++; this.storage.get(key, (data) => { if (data) this.flexPages.fromJSON(data); if (--pending === 0) this._finishInit(); }, true); } else if (type === "layout") { this.layoutMap.set(id, true); } }).bind(this)); if (pending === 0) this._finishInit(); // Trường hợp không có key nào là "pI" }).bind(this)); } _finishInit() { this.isReady = true; this.readyCallbacks.forEach(cb => cb()); this.readyCallbacks = []; } // Lấy thông tin trang hoặc layout // Lấy dữ liệu từ cache (localStorage hoặc IndexedDB nếu cần) get(id, callback, type = "cahce") { this._onReady(() => { this.storage.get(id, (data) => { if (!data) { callback(null); return; } callback(data); }, false, type); }); } searchFlexPage(url, callback) { this._onReady(() => { callback(this.flexPages.search(url)); }); } genShortID() { return Date.now().toString(36) + Math.random().toString(36).substring(2, 5); } // Lưu dữ liệu page hoặc layout vào cache set(id, obj, type = "page", callback) { this._onReady(() => { const key = this._key(id, type); if (type === "page") { const htmlData = { html: obj.html, doc: obj.doc.innerHTML }; let info = { title: obj.title, idPage: obj.idPage, lName: obj.lName, dynamicF: obj.dynamicF }; // Lưu dữ liệu htmlData vào storage const idN = this.genShortID(); let url = window.getPathFromUrl(id); if (obj.flexPageID !== "None" && obj.flexPageID !== "") { if (obj.dynamicF !== null && obj.dynamicF !== undefined && obj.dynamicF != "") { url = obj.flexPageID; info = { lName: obj.lName, dynamicF: obj.dynamicF } } } this.flexPages.insert(url, { "linkID": idN, "info": info }); //this.storage.set(idN, htmlData, function () { }, type = "flex"); if (this.countFP > 5) { this.countFP = 0; //this.storage.set("treePage", this.flexPages.toJSON(), function () { }); } else { this.countFP++; } this.storage.set("treePage", this.flexPages.toJSON(), function () { }); } else { const layout = { Content: obj.Content, Scripts: obj.Scripts }; // Lưu dữ liệu layout vào storage this.layoutMap.set(id, true); this.storage.set(key, layout, function () { // callback(null); // Gọi callback khi lưu thành công }); } }); } } class CacheStorage { constructor(prefix = "cache", maxAgeMs = 3 * 24 * 60 * 60 * 1000) { this.prefix = prefix; this.maxAgeMs = maxAgeMs; this.db = null; this.useIndexedDB = false; this.queue = []; this.ready = false; this._initDB(); this.cleanupExpired(); } _key(key) { return `${this.prefix}|${key}`; } _initDB() { const openDB = () => { console.log("Renew Database"); const req = indexedDB.open(`${this.prefix}_db`, 1); req.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains("cache")) { db.createObjectStore("cache", { keyPath: "key" }); } if (!db.objectStoreNames.contains("fpCache")) { db.createObjectStore("fpCache", { keyPath: "key" }); } }; req.onsuccess = () => { this.db = req.result; this.ready = true; this._flushQueue(); }; req.onerror = () => { console.warn("IndexedDB open failed, fallback to localStorage"); this.db = null; this.ready = true; this._flushQueue(); }; }; const del = indexedDB.deleteDatabase(`${this.prefix}_db`); del.onsuccess = () => openDB(); del.onerror = () => { console.warn("deleteDatabase failed, continue opening fresh"); openDB(); }; del.onblocked = () => { console.warn("deleteDatabase blocked (tab khác đang giữ kết nối)"); }; } _flushQueue() { while (this.queue.length) { const fn = this.queue.shift(); fn(); } } _onReady(fn) { if (this.ready) fn(); else this.queue.push(fn); } get(key, callback, f = false, type = "cache") { this._onReady(() => { if (this.db) { let tx, store, req; if (type === "flex") { tx = this.db.transaction("fpCache", "readonly"); store = tx.objectStore("fpCache"); } else { tx = this.db.transaction("cache", "readonly"); store = tx.objectStore("cache"); } req = store.get(f ? key : this._key(key)); req.onsuccess = () => callback(req.result ? req.result.value : null); } else { try { const raw = localStorage.getItem(this._key(key)); const parsed = raw ? JSON.parse(raw) : null; callback(parsed.data); } catch { callback(null); } } }); } getAllKeys(callback) { this._onReady(() => { if (this.db) { let tx = this.db.transaction("cache", "readonly"); let store = tx.objectStore("cache"); let req = store.getAllKeys(); req.onsuccess = (evt) => { callback(evt.target.result); }; } }); } set(key, data, callback = () => { }, type="cache") { this._onReady(() => { if (this.db) { let tx, store, req; if (type === "flex") { tx = this.db.transaction("fpCache", "readwrite"); store = tx.objectStore("fpCache"); store.put({ key: this._key(key), value: data, ts: Date.now() }); } else { tx = this.db.transaction("cache", "readwrite"); store = tx.objectStore("cache"); store.put({ key: this._key(key), value: data, ts: Date.now() }); } tx.oncomplete = () => callback(null); tx.onerror = (e) => callback(e); } else { try { console.log("Run Local"); localStorage.setItem(this._key(key), JSON.stringify({ data, ts: Date.now() })); callback(null); } catch (e) { callback(e); } } }); } remove(key, type = "cache") { const fullKey = this._key(key); if (this.db) { try { const tx = this.db.transaction("cache", "readwrite"); const store = tx.objectStore("cache"); store.delete(fullKey); } catch { } } } cleanupExpired() { const now = Date.now(); for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (!key.startsWith(this.prefix)) continue; try { const raw = JSON.parse(localStorage.getItem(key)); if (raw && raw.ts && now - raw.ts > this.maxAgeMs) { localStorage.removeItem(key); } } catch { localStorage.removeItem(key); } } } } class TrieNode { constructor() { this.children = new Map(); // segment -> TrieNode this.paramChild = null; // node chứa ':param' this.wildcardChild = null; // node chứa '*' this.paramName = null; // tên param nếu là param node this.layout = null; } } class FlexPageTrie { constructor() { this.root = new TrieNode(); } insert(path, layout) { const segments = path.split('/').filter(Boolean); let node = this.root; for (const seg of segments) { if (seg === '*') { if (!node.wildcardChild) node.wildcardChild = new TrieNode(); node = node.wildcardChild; } else if (seg.startsWith(':')) { if (!node.paramChild) node.paramChild = new TrieNode(); node.paramChild.paramName = seg.slice(1); node = node.paramChild; } else { if (!node.children.has(seg)) node.children.set(seg, new TrieNode()); node = node.children.get(seg); } } node.layout = layout; } search(url) { const segments = url.split('/').filter(Boolean); const result = this._searchRecursive(this.root, segments, 0, {}); return result || null; } _searchRecursive(node, segments, index, params) { if (!node) return null; if (index === segments.length) { if (node.layout) { return { layout: node.layout, params }; } return null; } const seg = segments[index]; // Ưu tiên match chính xác if (node.children.has(seg)) { const res = this._searchRecursive(node.children.get(seg), segments, index + 1, { ...params }); if (res) return res; } // Match dynamic :param if (node.paramChild) { const newParams = { ...params }; newParams[node.paramChild.paramName] = seg; const res = this._searchRecursive(node.paramChild, segments, index + 1, newParams); if (res) return res; } // Match wildcard * if (node.wildcardChild) { const res = this._searchRecursive(node.wildcardChild, segments, index + 1, { ...params }); if (res) return res; } return null; } _toJSON(node) { const obj = { layout: node.layout, paramName: node.paramName, children: {}, paramChild: node.paramChild ? this._toJSON(node.paramChild) : null, wildcardChild: node.wildcardChild ? this._toJSON(node.wildcardChild) : null }; for (const [key, child] of node.children.entries()) { obj.children[key] = this._toJSON(child); } return obj; } toJSON() { return this._toJSON(this.root); } _fromJSON(data) { const node = new TrieNode(); node.layout = data.layout; node.paramName = data.paramName; for (const key in data.children) { node.children.set(key, this._fromJSON(data.children[key])); } if (data.paramChild) { node.paramChild = this._fromJSON(data.paramChild); } if (data.wildcardChild) { node.wildcardChild = this._fromJSON(data.wildcardChild); } return node; } fromJSON(data) { this.root = this._fromJSON(data); } } window.removeStopCollapsed = function () { if (window.dropdown != null && window.dropdown.currentE != null) { if (window.dropdown.currentE.item.hasAttribute("stopCollapsed")) { window.dropdown.currentE.item.removeAttribute("stopCollapsed"); window.dropdown.currentE = null; } } } window.scroll_options = { damping: 0.1, thumbMinSize: 25, renderByPixel: true, alwaysShowTracks: true, continuousScrolling: true, plugins: { overscroll: { effect: 'bounce', damping: 0.15, maxOverscroll: 150 } } };