1270 lines
41 KiB
JavaScript
1270 lines
41 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();
|
|
}
|
|
|
|
_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) {
|
|
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, false);
|
|
if (parent) {
|
|
if (!this.parentEventMap.has(parent)) {
|
|
this.parentEventMap.set(parent, new Set());
|
|
}
|
|
this.parentEventMap.get(parent).add({ element, eventName });
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
|
|
|
var sOption = {
|
|
damping: (window.getOS() == "Android") ? .1 : .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) {
|
|
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) => {
|
|
console.log(result);
|
|
|
|
if (result1) {
|
|
console.log(result);
|
|
this.setContentPage(result1, 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]");
|
|
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) {
|
|
this.cachePage.getInf(window.location.href, ((tmp) => {
|
|
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(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
|
|
getInf(id, callback, type = "page") {
|
|
this._onReady(() => {
|
|
(type === "page") ? callback(this.pageMap.get(id)) : callback(this.layoutMap.get(id));
|
|
});
|
|
}
|
|
|
|
// Lấy dữ liệu từ cache (localStorage hoặc IndexedDB nếu cần)
|
|
get(id, callback, type = "page") {
|
|
this._onReady(() => {
|
|
const key = this._key(id, type);
|
|
this.storage.get(key, (data) => {
|
|
if (!data) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
callback(data);
|
|
});
|
|
});
|
|
}
|
|
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
|
|
};
|
|
const 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 = (obj.flexPageID !== "None") ? obj.flexPageID : window.getPathFromUrl(id);
|
|
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 req = indexedDB.open(`${this.prefix}_db`, 1);
|
|
req.onupgradeneeded = (e) => {
|
|
let store;
|
|
if (!e.target.result.objectStoreNames.contains("cache")) {
|
|
store = e.target.result.createObjectStore("cache", { keyPath: "key" });
|
|
}
|
|
if (!e.target.result.objectStoreNames.contains("fpCache")) {
|
|
e.target.result.createObjectStore("fpCache", { keyPath: "key" });
|
|
}
|
|
try {
|
|
store.add({ id: 1, value: "test data" });
|
|
store.delete("1");
|
|
} catch (e) {
|
|
this.ready = true;
|
|
this.db = null;
|
|
this.useIndexedDB = false;
|
|
}
|
|
|
|
};
|
|
req.onsuccess = (event) => {
|
|
this.db = req.result;
|
|
this.ready = true;
|
|
this._flushQueue();
|
|
};
|
|
req.onerror = () => {
|
|
console.warn("IndexedDB failed, fallback to localStorage");
|
|
this.ready = true;
|
|
this.db = null;
|
|
this._flushQueue();
|
|
};
|
|
}
|
|
_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
|
|
}
|
|
}
|
|
}; |