Files
TWA-App/TWASys-App/wwwroot/js/libs/js-core.js
2025-10-22 09:41:40 +07:00

1299 lines
42 KiB
JavaScript

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<id, callback>
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
}
}
};