update news & events
This commit is contained in:
@ -1,307 +1,601 @@
|
||||
export default class AGrid extends window.AObject {
|
||||
export default class AGrid extends window.AObject {
|
||||
constructor(options) {
|
||||
super();
|
||||
const defaultO = { "element": ".AGrid", "gridType": "row", "hasRatio": false, "desiredHeight": "200", "gutter": "10" };
|
||||
this.ele = document.querySelector(options.element);
|
||||
const defaultO = { element: ".AGrid", gridType: "row", hasRatio: false, desiredHeight: 200, gutter: 20 };
|
||||
this._o = { ...defaultO, ...options };
|
||||
this.gridWidth = this.ele.clientWidth;
|
||||
this.keyI = 1;
|
||||
this.isRender = false;
|
||||
this.listGrid = new Map();
|
||||
this.loaded = false;
|
||||
|
||||
|
||||
this.ele = (typeof this._o.element === "string") ? document.querySelector(this._o.element) : this._o.element;
|
||||
if (!this.ele) throw new Error("AGrid: element not found");
|
||||
|
||||
|
||||
this.timeout = false;
|
||||
this.ticking = false;
|
||||
this.timeout = false
|
||||
this.isRender = false;
|
||||
this.loaded = false;
|
||||
this.cancelFunc = null;
|
||||
this.ele.querySelectorAll(".grid-item").forEach(el => {
|
||||
var img = el.querySelector("img.mImg");
|
||||
this.listGrid.set(this.keyI, { "gridEl": el, "imgEl": img, "aRatio": el.hasAttribute("aRatio") ? el.getAttribute("aRatio") : 0, "status": 0, "bounding": null });
|
||||
var kt = this.keyI;
|
||||
let f = (function (e) {
|
||||
let k = kt;
|
||||
this.listGrid.get(kt).status = 1;
|
||||
this.listGrid.get(kt).aRatio = e.currentTarget.clientWidth / e.currentTarget.clientHeight;
|
||||
this.listGrid.get(kt).gridEl.setAttribute("aRatio", this.listGrid.get(k).aRatio);
|
||||
this.listGrid.get(kt).bounding = e.currentTarget.getBoundingClientRect();
|
||||
this.loadImage();
|
||||
this.removeSystemEvent(e.currentTarget);
|
||||
}).bind(this);
|
||||
this.addSystemEvent("load", img, f);
|
||||
if (img.complete) {
|
||||
f({ "currentTarget": img });
|
||||
this.removeSystemEvent(img);
|
||||
this.cancelResize = null;
|
||||
|
||||
this.addSystemEvent("resize", window, this.reLayout.bind(this));
|
||||
window.app?.on?.("App_Scrolling", ((_) => {
|
||||
if (window.app.checkVisible(this.ele) && !this.isRender) this.renderAnimation();
|
||||
}).bind(this));
|
||||
|
||||
this.reElement();
|
||||
}
|
||||
|
||||
observerInit(options) {
|
||||
this._scrollEnd = {
|
||||
...
|
||||
{
|
||||
enabled: true, // tắt/mở tính năng
|
||||
root: null, // null = viewport; có thể truyền 1 element scroll container
|
||||
rootMargin: "0px 0px 200px 0px", // nới 200px ở đáy để bắt sớm
|
||||
threshold: 0.01, // chỉ cần chạm nhẹ là báo
|
||||
onceUntilAppend: true // chỉ bắn 1 lần cho mỗi lần append; reset khi append thêm
|
||||
}, ...options
|
||||
};
|
||||
this._io = null; // IntersectionObserver instance
|
||||
this._ioTriggered = false;
|
||||
}
|
||||
_setupScrollEndIO() {
|
||||
if (!this._scrollEnd.enabled) return;
|
||||
if (this._io) return;
|
||||
this._io = new IntersectionObserver(this._onScrollEndIntersect.bind(this), {
|
||||
root: this._scrollEnd.root || null,
|
||||
rootMargin: this._scrollEnd.rootMargin,
|
||||
threshold: this._scrollEnd.threshold
|
||||
});
|
||||
}
|
||||
_getLastGridItemEl() {
|
||||
// Ưu tiên DOM order (an toàn cho append)
|
||||
let el = this.ele.querySelector(".grid-item:last-of-type");
|
||||
if (el) return el;
|
||||
// Fallback theo Map (key lớn nhất)
|
||||
if (this.listGrid && this.listGrid.size) {
|
||||
let last = null, lastKey = -Infinity;
|
||||
for (const [k, rec] of this.listGrid) { if (k > lastKey) { lastKey = k; last = rec; } }
|
||||
if (last) return last.gridEl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bắt đầu quan sát item cuối
|
||||
_observeLastItem() {
|
||||
if (!this._scrollEnd?.enabled) return;
|
||||
this._setupScrollEndIO();
|
||||
if (!this._io) return;
|
||||
|
||||
const lastEl = this._getLastGridItemEl();
|
||||
this._io.disconnect(); // luôn quan sát lại đúng item cuối
|
||||
if (lastEl) this._io.observe(lastEl);
|
||||
|
||||
// Nếu dùng chế độ bắn 1 lần đến khi append thêm:
|
||||
if (this._scrollEnd.onceUntilAppend) this._ioTriggered = false;
|
||||
}
|
||||
|
||||
// Xử lý khi giao cắt
|
||||
_onScrollEndIntersect(entries) {
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
if (this._ioTriggered && this._scrollEnd.onceUntilAppend) return;
|
||||
|
||||
this._ioTriggered = true;
|
||||
//console.log(this);
|
||||
this.trigger("scrollEnd", { instance: this });
|
||||
|
||||
// Nếu không dùng onceUntilAppend thì vẫn muốn chống spam khi còn trong khung:
|
||||
if (!this._scrollEnd.onceUntilAppend) {
|
||||
setTimeout(() => { this._ioTriggered = false; }, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public: cho phép tay reset cờ (nếu cần)
|
||||
resetScrollEndTrigger() { this._ioTriggered = false; }
|
||||
|
||||
// Public: tắt/bật nhanh
|
||||
enableScrollEnd(v = true) {
|
||||
this._scrollEnd.enabled = !!v;
|
||||
if (!v && this._io) { this._io.disconnect(); }
|
||||
if (v) { this._setupScrollEndIO(); this._observeLastItem(); }
|
||||
}
|
||||
|
||||
// ===== Khởi tạo lại list từ DOM hiện có =====
|
||||
reElement() {
|
||||
this.gridWidth = this.ele.clientWidth;
|
||||
if (this._o.gridType === "waterfall") {
|
||||
this.initCols();
|
||||
}
|
||||
this.listGrid = new Map();
|
||||
this.keyI = 1;
|
||||
|
||||
const imageLoadPromises = [];
|
||||
this.ele.querySelectorAll(".grid-item").forEach((el) => {
|
||||
const img = el.querySelector("img.mImg");
|
||||
const key = this.keyI;
|
||||
|
||||
this.listGrid.set(key, {
|
||||
gridEl: el,
|
||||
imgEl: img || null,
|
||||
aRatio: el.hasAttribute("aRatio") ? parseFloat(el.getAttribute("aRatio")) || 0 : 0,
|
||||
status: 0,
|
||||
bounding: el.getBoundingClientRect()
|
||||
});
|
||||
|
||||
const p = new Promise((resolve) => {
|
||||
if (!img) {
|
||||
const rec = this.listGrid.get(key);
|
||||
rec.status = 1;
|
||||
if (!rec.aRatio || !isFinite(rec.aRatio)) { rec.aRatio = 1; rec.gridEl.setAttribute("aRatio", rec.aRatio); }
|
||||
resolve(); return;
|
||||
}
|
||||
|
||||
const done = () => {
|
||||
const rec = this.listGrid.get(key);
|
||||
rec.status = 1;
|
||||
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
||||
rec.aRatio = img.naturalWidth / img.naturalHeight;
|
||||
} else if (!rec.aRatio || !isFinite(rec.aRatio)) {
|
||||
rec.aRatio = 1;
|
||||
}
|
||||
rec.gridEl.setAttribute("aRatio", rec.aRatio);
|
||||
rec.bounding = img.getBoundingClientRect();
|
||||
this.removeSystemEvent?.("load", img, onLoad);
|
||||
this.removeSystemEvent?.("error", img, onError);
|
||||
resolve();
|
||||
};
|
||||
const onLoad = () => done();
|
||||
const onError = () => { const r = this.listGrid.get(key); r.status = -1; done(); };
|
||||
|
||||
if (typeof img.decode === "function") {
|
||||
img.decode().then(done).catch(() => {
|
||||
if (img.complete) done();
|
||||
else { this.addSystemEvent?.("load", img, onLoad); this.addSystemEvent?.("error", img, onError); }
|
||||
});
|
||||
} else if (img.complete) done();
|
||||
else { this.addSystemEvent?.("load", img, onLoad); this.addSystemEvent?.("error", img, onError); }
|
||||
});
|
||||
|
||||
imageLoadPromises.push(p);
|
||||
this.keyI++;
|
||||
});
|
||||
this.initRows();
|
||||
this.wWidth = window.innerWidth || document.body.clientWidth;
|
||||
this.options = options;
|
||||
this.addSystemEvent("resize", window, this.reLayout.bind(this));
|
||||
window.app.on("App_Scrolling", ((y) => {
|
||||
if (window.app.checkVisible(this.ele) && !this.isRender) {
|
||||
this.renderAnimation();
|
||||
|
||||
Promise.allSettled(imageLoadPromises).then(() => {
|
||||
this.renderAnimation();
|
||||
window.requestTimeout?.(this.layoutInit.bind(this), 100);
|
||||
});
|
||||
}
|
||||
appendElement(input) {
|
||||
const items = this._normalizeToElements(input);
|
||||
if (!items.length) return;
|
||||
|
||||
if (!this.listGrid) this.listGrid = new Map();
|
||||
if (typeof this.keyI !== "number" || this.keyI < 1) this.keyI = this.listGrid.size + 1;
|
||||
|
||||
const newKeys = [];
|
||||
|
||||
for (const el of items) {
|
||||
if (!el.classList.contains("grid-item")) el.classList.add("grid-item");
|
||||
this.ele.appendChild(el);
|
||||
|
||||
const { key, rec } = this._registerItemSync(el); // đăng ký ngay (đồng bộ)
|
||||
newKeys.push(key);
|
||||
|
||||
// ĐẶT VỊ TRÍ NGAY (không chờ ảnh) — giữ tối ưu single append
|
||||
if (this._o.gridType === "waterfall") {
|
||||
this._appendOptimizedSingleWaterfall(key, rec);
|
||||
this._observeLastItem();
|
||||
} else {
|
||||
if (!this.listRows?.length) { this.isRender = false; this.layoutInit(); this.renderAnimation?.(); }
|
||||
else this._appendOptimizedSingleRow(key, rec);
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
initGrid() {
|
||||
this.ele.style.height = this.ele.clientHeight + "px";
|
||||
const firstEntry = this.listGrid.entries().next().value;
|
||||
let refY = firstEntry[1].bounding.top;
|
||||
let refX = firstEntry[1].bounding.left;
|
||||
for (const [key, value] of this.listGrid) {
|
||||
let rect = value.bounding;
|
||||
let t = rect.top - refY;
|
||||
let l = rect.left - refX;
|
||||
value.gridEl.setAttribute("style", `position: absolute;top:${t}px;left:${l}; width:${rect.width};height:${rect.height}`);
|
||||
|
||||
// BẮT ĐẦU load ảnh BẤT ĐỒNG BỘ (nếu có)
|
||||
if (rec.imgEl) this._watchImageAsync(key, rec.imgEl);
|
||||
}
|
||||
|
||||
}
|
||||
reLayout(e) {
|
||||
if (this.gridWidth == this.ele.clientWidth) {
|
||||
return;
|
||||
|
||||
if (newKeys.length > 1) {
|
||||
this.isRender = false;
|
||||
this.layoutInit();
|
||||
if (window.app?.checkVisible?.(this.ele)) this.renderAnimation();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Đăng ký item ngay lập tức + aRatio tạm nếu cần
|
||||
_registerItemSync(el) {
|
||||
el.style.width = this.colW + "px";
|
||||
const img = el.querySelector("img.mImg");
|
||||
const key = this.keyI++;
|
||||
const rec = {
|
||||
gridEl: el,
|
||||
imgEl: img || null,
|
||||
aRatio: el.hasAttribute("aRatio") ? parseFloat(el.getAttribute("aRatio")) : 0,
|
||||
status: 0,
|
||||
bounding: el.getBoundingClientRect()
|
||||
};
|
||||
|
||||
if (img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) {
|
||||
// ảnh đã sẵn trong cache → dùng luôn tỉ lệ thật
|
||||
rec.aRatio = img.naturalWidth / img.naturalHeight;
|
||||
rec.status = 1;
|
||||
el.setAttribute("aRatio", rec.aRatio);
|
||||
rec.bounding = img.getBoundingClientRect();
|
||||
} else {
|
||||
// dùng tạm tỉ lệ 1 nếu thiếu
|
||||
if (!rec.aRatio || !isFinite(rec.aRatio)) {
|
||||
rec.aRatio = 1;
|
||||
el.setAttribute("aRatio", rec.aRatio);
|
||||
}
|
||||
rec.status = img ? 0 : 1; // 0: đang chờ ảnh, 1: không có ảnh
|
||||
}
|
||||
|
||||
this.listGrid.set(key, rec);
|
||||
return { key, rec };
|
||||
}
|
||||
|
||||
// Theo dõi load ảnh (BẤT ĐỒNG BỘ) → cập nhật tỉ lệ & re-layout (debounce)
|
||||
_watchImageAsync(key, img) {
|
||||
let done = false;
|
||||
const finalize = (ok) => {
|
||||
if (done) return; done = true;
|
||||
const rec = this.listGrid.get(key);
|
||||
if (!rec) return;
|
||||
if (ok && img.naturalWidth > 0 && img.naturalHeight > 0) {
|
||||
rec.aRatio = img.naturalWidth / img.naturalHeight;
|
||||
rec.gridEl.setAttribute("aRatio", rec.aRatio);
|
||||
rec.bounding = img.getBoundingClientRect();
|
||||
rec.status = 1;
|
||||
} else {
|
||||
// lỗi ảnh → giữ aRatio hiện tại (đã có), mark lỗi
|
||||
if (!rec.aRatio || !isFinite(rec.aRatio)) {
|
||||
rec.aRatio = 1;
|
||||
rec.gridEl.setAttribute("aRatio", rec.aRatio);
|
||||
}
|
||||
rec.status = -1;
|
||||
}
|
||||
|
||||
this.layoutInit(); // reflow lại sau khi ảnh sẵn sàng
|
||||
};
|
||||
|
||||
const onLoad = () => { cleanup(); finalize(true); };
|
||||
const onError = () => { cleanup(); finalize(false); };
|
||||
const cleanup = () => {
|
||||
img.removeEventListener("load", onLoad);
|
||||
img.removeEventListener("error", onError);
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
|
||||
// Ưu tiên decode (async), fallback event load/error
|
||||
if (typeof img.decode === "function") {
|
||||
img.decode().then(() => finalize(true)).catch(() => {
|
||||
if (img.complete) finalize(true);
|
||||
else {
|
||||
img.addEventListener("load", onLoad, { once: true });
|
||||
img.addEventListener("error", onError, { once: true });
|
||||
}
|
||||
});
|
||||
} else if (img.complete) {
|
||||
finalize(true);
|
||||
} else {
|
||||
img.addEventListener("load", onLoad, { once: true });
|
||||
img.addEventListener("error", onError, { once: true });
|
||||
}
|
||||
|
||||
// Fallback timeout nếu load/error không bắn
|
||||
/// const timer = setTimeout(() => finalize(false), 8000);
|
||||
}
|
||||
|
||||
_normalizeToElements(input) {
|
||||
if (typeof input === "string") {
|
||||
const tpl = document.createElement("template");
|
||||
tpl.innerHTML = input.trim();
|
||||
return Array.from(tpl.content.children);
|
||||
}
|
||||
if (input instanceof HTMLElement) return [input];
|
||||
if (input instanceof DocumentFragment) return Array.from(input.children);
|
||||
if (input && (input instanceof NodeList || input instanceof HTMLCollection || Array.isArray(input))) {
|
||||
return Array.from(input).filter(n => n instanceof HTMLElement);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ===== Layout chung =====
|
||||
layoutInit() {
|
||||
this.gridWidth = this.ele.clientWidth;
|
||||
this.isRender = false;
|
||||
if (this._o.gridType === "row") {
|
||||
this.initRows();
|
||||
this.layoutRows();
|
||||
} else {
|
||||
this.initCols();
|
||||
this.restructCol();
|
||||
this.layoutCols();
|
||||
}
|
||||
this._observeLastItem();
|
||||
}
|
||||
restructCol() {
|
||||
this.ele.style.height = 'auto';
|
||||
|
||||
this.listGrid.forEach((v, k) => {
|
||||
v.gridEl.style.top = '';
|
||||
v.gridEl.style.left = '';
|
||||
v.gridEl.style.width = this.colW;
|
||||
v.gridEl.style.height = '';
|
||||
v.gridEl.style.position = "relative";
|
||||
v.bounding = v.gridEl.getBoundingClientRect();
|
||||
});
|
||||
}
|
||||
reLayout() {
|
||||
if (this.cancelResize) this.cancelResize();
|
||||
if (!this.ticking) {
|
||||
if (this.cancelFunc != null) this.cancelFunc();
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.cancelFunc) this.cancelFunc();
|
||||
const animationId = window.requestAnimationFrame(() => {
|
||||
this.isRender = false;
|
||||
this.sTime = new Date();
|
||||
this.timeout = false;
|
||||
this.ticking = true;
|
||||
this.layoutInit();
|
||||
if (this.timeout == false) {
|
||||
this.timeout = true;
|
||||
this.resizeEnd();
|
||||
}
|
||||
this.ticking = false;
|
||||
});
|
||||
this.cancelFunc = (() => { cancelAnimationFrame(animationId); });
|
||||
window.requestTimeout(() => { if (this.cancelFunc) this.cancelFunc(); this.layoutInit(); }, 500, (t) => { this.cancelResize = t; });
|
||||
}
|
||||
}
|
||||
layoutInit() {
|
||||
this.gridWidth = this.ele.clientWidth;
|
||||
switch (this._o.gridType) {
|
||||
case "row":
|
||||
this.initRows();
|
||||
this.layoutRows();
|
||||
break;
|
||||
case "waterfall":
|
||||
this.restuctCol();
|
||||
this.initCols();
|
||||
this.layoutCols();
|
||||
break;
|
||||
}
|
||||
}
|
||||
resizeEnd() {
|
||||
var t = new Date() - this.sTime;
|
||||
if (t < 200) {
|
||||
window.requestTimeout(this.resizeEnd.bind(this), 100, (t) => { this.cancelFunc = t });
|
||||
} else {
|
||||
this.layoutInit();
|
||||
this.timeout = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ROW (Justified) =====
|
||||
initRows() {
|
||||
this.listRows = [];
|
||||
this.listRows.push(this.structureRow(this._o.desiredHeight));
|
||||
}
|
||||
initCols() {
|
||||
this.listCols = [];
|
||||
const firstEntry =this.listGrid.entries().next().value;
|
||||
this.colNum = Math.round(this.gridWidth / firstEntry[1].bounding.width);
|
||||
let colW = (this.gridWidth - (this.colNum - 1) * this._o.gutter) / this.colNum;
|
||||
for (let i = 0; i < this.colNum; i++) {
|
||||
const tm = this.structureCol(i + 1);
|
||||
tm.colW = colW;
|
||||
this.listCols.push(tm);
|
||||
}
|
||||
}
|
||||
restuctCol() {
|
||||
this.ele.style.height = '';
|
||||
|
||||
this.listGrid.forEach((v, k) => {
|
||||
v.gridEl.style.top = '';
|
||||
v.gridEl.style.left = '';
|
||||
v.gridEl.style.width = '';
|
||||
v.gridEl.style.height = '';
|
||||
v.gridEl.style.position = "relative";
|
||||
v.bounding = v.gridEl.getBoundingClientRect();
|
||||
});
|
||||
this.initGrid();
|
||||
}
|
||||
loadImage() {
|
||||
if (this._o.hasRatio) {
|
||||
|
||||
} else {
|
||||
window.requestTimeout((() => {
|
||||
if (this.loaded) return;
|
||||
for (const [key, value] of this.listGrid) {
|
||||
if (value.status == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.loaded = true;
|
||||
//if (this._o.gridType == "waterfall") console.log("waterfall " + this.loaded);
|
||||
//else console.log("Rows: " + this.loaded);
|
||||
this.initGrid();
|
||||
switch (this._o.gridType) {
|
||||
case "row":
|
||||
this.layoutRows();
|
||||
break;
|
||||
case "waterfall":
|
||||
this.initCols();
|
||||
this.layoutCols();
|
||||
break;
|
||||
}
|
||||
}).bind(this), 100);
|
||||
}
|
||||
}
|
||||
layoutRows() {
|
||||
var nRow = 0;
|
||||
this.listGrid.forEach((v, k) => {
|
||||
nRow = this.pushEleToRow(nRow, k);
|
||||
});
|
||||
let nRow = 0;
|
||||
this.listGrid.forEach((_, k) => { nRow = this.pushEleToRow(nRow, k); });
|
||||
this.ele.style.height = this.calcPosTop(0, this.listRows.length) + "px";
|
||||
if (window.app.checkVisible(this.ele)) {
|
||||
this.renderAnimation();
|
||||
}
|
||||
}
|
||||
layoutCols() {
|
||||
let i = 0;
|
||||
let col = 0;
|
||||
let l = 0;
|
||||
//console.log(this.listgrid);
|
||||
//console.log(this.listcols);
|
||||
this.listGrid.forEach((v, k) => {
|
||||
let t = 0;
|
||||
let cVal;
|
||||
if (i % this.colNum == 0) {
|
||||
col = 0;
|
||||
cVal = this.listCols[col];
|
||||
l = 0;
|
||||
} else {
|
||||
col++;
|
||||
cVal = this.listCols[col];
|
||||
l += this.listCols[col].colW + 1*this._o.gutter;
|
||||
}
|
||||
t = cVal.colH;
|
||||
if (i > this.colNum - 1) {
|
||||
t += 1*this._o.gutter;
|
||||
}
|
||||
cVal.listElement.push({ "key": k, "top": t, "left": l, "render": false });
|
||||
cVal.colH = t + cVal.colW / v.aRatio;
|
||||
i++;
|
||||
});
|
||||
|
||||
this.ele.style.height = this.getColMaxHeight() + "px";
|
||||
if (window.app.checkVisible(this.ele)) {
|
||||
this.renderAnimation();
|
||||
}
|
||||
}
|
||||
getColMaxHeight() {
|
||||
let m = 0;
|
||||
for (let i = 0; i < this.listCols.length; i++) {
|
||||
if (m <= this.listCols[i].colH) {
|
||||
m = this.listCols[i].colH;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
popEle(nRow, pos, k) {
|
||||
var cRow = this.listRows[nRow];
|
||||
var listPop = [];
|
||||
let posEle = 0;
|
||||
for (let i = 0; i < cRow.listElement.length; i++) {
|
||||
if (cRow.listElement[i].key > k) {
|
||||
posEle = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cRow.listElement.slice(posEle, this.listElement.length - posEle);
|
||||
this.listRows.slice(nRow + 1, this.listRows.length - nRow);
|
||||
|
||||
}
|
||||
renderAnimation() {
|
||||
this.isRender = true;
|
||||
switch (this._o.gridType) {
|
||||
case "row":
|
||||
this.listRows.forEach((el) => {
|
||||
for (let i = 0; i < el.listElement.length; i++) {
|
||||
let grid = this.listGrid.get(el.listElement[i].key);
|
||||
var w = parseFloat(el.rowH * grid.aRatio).toFixed(2);
|
||||
grid.gridEl.setAttribute("style", `position: absolute; top: ${parseFloat(el.listElement[i].top).toFixed(2)}px; left: ${parseFloat(el.listElement[i].left).toFixed(2)}px; width:${w}px; height:${parseFloat(el.rowH).toFixed(2)}px`);
|
||||
grid.imgEl.setAttribute("style", `width:${w}px; height:${parseFloat(el.rowH).toFixed(2)}px`);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "waterfall":
|
||||
this.listCols.forEach((el) => {
|
||||
for (let i = 0; i < el.listElement.length; i++) {
|
||||
let grid = this.listGrid.get(el.listElement[i].key);
|
||||
grid.gridEl.setAttribute("style", `position: absolute; top: ${parseFloat(el.listElement[i].top).toFixed(2)}px; left: ${parseFloat(el.listElement[i].left).toFixed(2)}px; width:${el.colW}px; height:${parseFloat(el.colW / grid.aRatio).toFixed(2)}px`);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (window.app?.checkVisible?.(this.ele)) this.renderAnimation();
|
||||
}
|
||||
|
||||
pushEleToRow(nRow, k) {
|
||||
let grid = this.listGrid.get(k);
|
||||
var cRow = this.listRows[nRow];
|
||||
let t1 = this._o.desiredHeight * grid.aRatio;
|
||||
let rowH = cRow.rowH;
|
||||
var errorH = (cRow.rowW + t1) / (this.gridWidth - (cRow.listElement.length - 1) * (this._o.gutter));
|
||||
|
||||
if (errorH > 1.2) {
|
||||
if (cRow.listElement.length != 0) {
|
||||
cRow.rowH = this.reCalcWidth(cRow);
|
||||
this.listRows.push(this.structureRow(this._o.desiredHeight));
|
||||
return this.pushEleToRow(nRow + 1, k);
|
||||
}
|
||||
const grid = this.listGrid.get(k);
|
||||
const cRow = this.listRows[nRow];
|
||||
const t1 = this._o.desiredHeight * (grid.aRatio * 1);
|
||||
const denom = this.gridWidth - ((cRow.listElement.length - 1) * (1 * this._o.gutter));
|
||||
const errorH = (cRow.rowW + t1) / (denom <= 0 ? 1 : denom);
|
||||
|
||||
if (errorH > 1.2 && cRow.listElement.length !== 0) {
|
||||
cRow.rowH = this.reCalcWidth(cRow);
|
||||
this.listRows.push(this.structureRow(this._o.desiredHeight));
|
||||
return this.pushEleToRow(nRow + 1, k);
|
||||
}
|
||||
var g = 0;
|
||||
if (cRow.listElement.length > 0) {
|
||||
g = this._o.gutter;
|
||||
}
|
||||
cRow.listElement.push({ "key": k, "top": this.calcPosTop(0, nRow), "left": (cRow.rowW + g), "render": false });
|
||||
|
||||
const g = (cRow.listElement.length > 0) ? this._o.gutter * 1 : 0;
|
||||
cRow.listElement.push({ key: k, top: this.calcPosTop(0, nRow), left: (cRow.rowW + g), render: false });
|
||||
cRow.rowW += t1;
|
||||
cRow.numM = k;
|
||||
if (errorH <= 1.2 && errorH >= 0.8) {
|
||||
cRow.rowH = this.reCalcWidth(cRow);
|
||||
}
|
||||
|
||||
if (errorH <= 1.2 && errorH >= 0.8) cRow.rowH = this.reCalcWidth(cRow);
|
||||
return nRow;
|
||||
}
|
||||
calcPosTop(begin, end) {
|
||||
if (begin == 0 && end == 0) {
|
||||
return 0;
|
||||
}
|
||||
let rowH = 0;
|
||||
for (let i = begin; i <= end - 1; i++) {
|
||||
rowH += 1*this.listRows[i].rowH + parseFloat(this._o.gutter);
|
||||
}
|
||||
return rowH;
|
||||
|
||||
calcPosTop(begin, end) {
|
||||
if (begin === 0 && end === 0) return 0;
|
||||
let rowH = 0;
|
||||
for (let i = begin; i <= end - 1; i++) rowH += this.listRows[i].rowH + 1 * this._o.gutter;
|
||||
return rowH;
|
||||
}
|
||||
|
||||
reCalcWidth(cRow) {
|
||||
let rowW = this.gridWidth - (cRow.listElement.length - 1) * (this._o.gutter);
|
||||
const rowW = this.gridWidth - (cRow.listElement.length - 1) * this._o.gutter * 1;
|
||||
let tRate = 0;
|
||||
for (var i = 0; i < cRow.listElement.length; i++) {
|
||||
tRate += this.listGrid.get(cRow.listElement[i].key).aRatio;
|
||||
}
|
||||
var rrowH = rowW / ((tRate == 0) ? 1 : tRate);
|
||||
for (var i = 0; i < cRow.listElement.length; i++) {
|
||||
if (i == 0) {
|
||||
cRow.listElement[i].left = 0;
|
||||
continue;
|
||||
for (let i = 0; i < cRow.listElement.length; i++) tRate += this.listGrid.get(cRow.listElement[i].key).aRatio * 1;
|
||||
const rrowH = rowW / (tRate === 0 ? 1 : tRate);
|
||||
|
||||
cRow.rowW = 0;
|
||||
for (let i = 0; i < cRow.listElement.length; i++) {
|
||||
if (i === 0) cRow.listElement[i].left = 0;
|
||||
else {
|
||||
const prev = cRow.listElement[i - 1];
|
||||
const wPrev = (this.listGrid.get(prev.key).aRatio * 1) * rrowH;
|
||||
cRow.rowW += wPrev;
|
||||
cRow.listElement[i].left = prev.left + wPrev + this._o.gutter * 1;
|
||||
}
|
||||
var t1 = cRow.listElement[i - 1];
|
||||
let w1 = this.listGrid.get(t1.key).aRatio * rrowH;
|
||||
cRow.rowW += w1;
|
||||
cRow.listElement[i].left = 1*t1.left + w1 + 1*this._o.gutter;
|
||||
}
|
||||
return rrowH;
|
||||
}
|
||||
structureCol(colN) {
|
||||
return { "listElement": [], "colNum": colN, "colH": 0, "colW":0 };
|
||||
|
||||
_appendOptimizedSingleRow(key, rec) {
|
||||
let lastIdx = this.listRows.length - 1;
|
||||
if (lastIdx < 0) { this.initRows(); lastIdx = 0; }
|
||||
const cRow = this.listRows[lastIdx];
|
||||
|
||||
const t1 = this._o.desiredHeight * (rec.aRatio || 1);
|
||||
const denom = this.gridWidth - (cRow.listElement.length - 1) * this._o.gutter;
|
||||
const errorH = (cRow.rowW + t1) / (denom <= 0 ? 1 : denom);
|
||||
const topBase = this.calcPosTop(0, lastIdx);
|
||||
|
||||
if (errorH > 1.2 && cRow.listElement.length !== 0) {
|
||||
cRow.rowH = this.reCalcWidth(cRow);
|
||||
// render lại row cũ
|
||||
for (let i = 0; i < cRow.listElement.length; i++) {
|
||||
const it = cRow.listElement[i];
|
||||
const g = this.listGrid.get(it.key);
|
||||
const w = cRow.rowH * (g.aRatio || 1);
|
||||
this._applyGridStyle(g.gridEl, it.top, it.left, w, cRow.rowH);
|
||||
if (g.imgEl) { g.imgEl.style.width = `${w.toFixed(2)}px`; g.imgEl.style.height = `${(+cRow.rowH).toFixed(2)}px`; }
|
||||
}
|
||||
|
||||
const newRow = this.structureRow(this._o.desiredHeight);
|
||||
this.listRows.push(newRow);
|
||||
const topNew = this.calcPosTop(0, this.listRows.length - 1);
|
||||
const leftNew = 0;
|
||||
newRow.listElement.push({ key, top: topNew, left: leftNew, render: false });
|
||||
newRow.rowW += t1;
|
||||
|
||||
const wNew = this._o.desiredHeight * (rec.aRatio || 1);
|
||||
this._applyGridStyle(rec.gridEl, topNew, leftNew, wNew, this._o.desiredHeight);
|
||||
} else {
|
||||
const gspace = cRow.listElement.length > 0 ? this._o.gutter : 0;
|
||||
const left = cRow.rowW + gspace;
|
||||
cRow.listElement.push({ key, top: topBase, left, render: false });
|
||||
cRow.rowW += t1;
|
||||
|
||||
if (errorH <= 1.2 && errorH >= 0.8) cRow.rowH = this.reCalcWidth(cRow);
|
||||
|
||||
for (let i = 0; i < cRow.listElement.length; i++) {
|
||||
const it = cRow.listElement[i];
|
||||
const g = this.listGrid.get(it.key);
|
||||
const w = cRow.rowH * (g.aRatio || 1);
|
||||
this._applyGridStyle(g.gridEl, it.top, it.left, w, cRow.rowH);
|
||||
if (g.imgEl) { g.imgEl.style.width = `${w.toFixed(2)}px`; g.imgEl.style.height = `${(+cRow.rowH).toFixed(2)}px`; }
|
||||
}
|
||||
}
|
||||
|
||||
this.ele.style.height = this.calcPosTop(0, this.listRows.length) + "px";
|
||||
this.isRender = true;
|
||||
}
|
||||
structureRow(rowH = 0) {
|
||||
return { "listElement": [], "rowH": rowH, "status": 0, "numM": 0, "rowW": 0 };
|
||||
|
||||
// ===== WATERFALL (Masonry) =====
|
||||
initCols() {
|
||||
this.wWidth = window.innerWidth || document.body.clientWidth;
|
||||
const colNum = this._resolveColNum(this.wWidth);
|
||||
this._buildCols(colNum, this.gridWidth);
|
||||
}
|
||||
}
|
||||
|
||||
layoutCols() {
|
||||
// phân bổ tuần tự sang cột (theo code gốc)
|
||||
let i = 0, col = 0, l = 0;
|
||||
this.listCols.forEach(c => { c.listElement = []; c.colH = 0; });
|
||||
|
||||
this.listGrid.forEach((v, k) => {
|
||||
let t = 0, cVal;
|
||||
const pC = v.gridEl.querySelector(".grid-content");
|
||||
let hC = pC ? pC.clientHeight : 0;
|
||||
if (i % this.listCols.length === 0) { col = 0; cVal = this.listCols[col]; l = 0; }
|
||||
else { col++; cVal = this.listCols[col]; l += this.colW + 1 * this._o.gutter; }
|
||||
|
||||
t = cVal.colH;
|
||||
hC = hC + this.colW / (1 * v.aRatio);
|
||||
// this._applyGridStyle(v.gridEl, t, l, this.colW, hC);
|
||||
cVal.listElement.push({ key: k, top: t, left: l, height: hC, render: false });
|
||||
cVal.colH = t + hC + 1 * this._o.gutter;
|
||||
i++;
|
||||
});
|
||||
|
||||
this.ele.style.height = this.getColMaxHeight() + "px";
|
||||
if (window.app?.checkVisible?.(this.ele)) this.renderAnimation();
|
||||
}
|
||||
_resolveColNum(width) {
|
||||
const bp = this._o.breakpoints;
|
||||
|
||||
|
||||
if (bp && typeof bp === "object") {
|
||||
const keys = Object.keys(bp)
|
||||
.map(k => +k)
|
||||
.filter(Number.isFinite)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
for (const k of keys) {
|
||||
if (width >= k) {
|
||||
const c = +bp[k]?.col;
|
||||
if (c > 0) return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
_buildCols(colNum, width) {
|
||||
const gutter = Number(this._o.gutter) || 0;
|
||||
this.colW = (width - (colNum - 1) * gutter) / colNum;
|
||||
this.listCols = [];
|
||||
let l = 0;
|
||||
for (let i = 0; i < colNum; i++) {
|
||||
this.listCols.push({ listElement: [], colH: 0, left: l, colW: this.colW });
|
||||
l += this.colW + 1 * this._o.gutter;
|
||||
}
|
||||
}
|
||||
_appendOptimizedSingleWaterfall(key, rec) {
|
||||
// Chọn cột thấp nhấtlistCols
|
||||
let minIdx = 0;
|
||||
for (let i = 1; i < this.listCols.length; i++) {
|
||||
if (this.listCols[i].colH < this.listCols[minIdx].colH) minIdx = i;
|
||||
}
|
||||
const col = this.listCols[minIdx];
|
||||
const pC = rec.gridEl.querySelector(".grid-content");
|
||||
const hC = pC ? pC.getBoundingClientRect().height : 0;
|
||||
|
||||
const height = (rec.aRatio == 0) ? rec.gridEl.clientHeight : hC + (this.colW / (1 * rec.aRatio));
|
||||
|
||||
const top = col.colH;
|
||||
const left = col.left;
|
||||
|
||||
col.listElement.push({ key, top, left, height, render: true });
|
||||
col.colH = top + height + 1 * (this._o.gutter || 0);
|
||||
this._applyGridStyle(rec.gridEl, top, left, this.colW, height);
|
||||
this.ele.style.height = this.getColMaxHeight() + "px";
|
||||
this.isRender = true;
|
||||
}
|
||||
getColMaxHeight() {
|
||||
let m = 0;
|
||||
for (let i = 0; i < this.listCols.length; i++) if (m <= this.listCols[i].colH) m = this.listCols[i].colH;
|
||||
return m;
|
||||
}
|
||||
|
||||
// ===== Render chung =====
|
||||
renderAnimation() {
|
||||
this.isRender = true;
|
||||
if (this._o.gridType === "row") {
|
||||
if (this.listRows == null) return;
|
||||
this.listRows.forEach((row) => {
|
||||
for (let i = 0; i < row.listElement.length; i++) {
|
||||
const it = row.listElement[i];
|
||||
const grid = this.listGrid.get(it.key);
|
||||
const w = row.rowH * (grid.aRatio || 1);
|
||||
this._applyGridStyle(grid.gridEl, it.top, it.left, w, row.rowH);
|
||||
if (grid.imgEl) {
|
||||
grid.imgEl.style.width = `${w.toFixed(2)}px`;
|
||||
grid.imgEl.style.height = `${(+row.rowH).toFixed(2)}px`;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (this.listCols == null) return;
|
||||
this.listCols.forEach((col) => {
|
||||
for (let i = 0; i < col.listElement.length; i++) {
|
||||
const it = col.listElement[i];
|
||||
const grid = this.listGrid.get(it.key);
|
||||
this._applyGridStyle(grid.gridEl, it.top, it.left, this.colW, it.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_applyGridStyle(el, top, left, width, height) {
|
||||
const s = el.style;
|
||||
if (s.position !== "absolute") s.position = "absolute";
|
||||
s.top = `${(+top).toFixed(2)}px`;
|
||||
s.left = `${(+left).toFixed(2)}px`;
|
||||
s.width = `${(+width).toFixed(2)}px`;
|
||||
s.height = `${(+height).toFixed(2)}px`;
|
||||
}
|
||||
structureCol(colN) { return { listElement: [], colNum: colN, colH: 0, colW: 0 }; }
|
||||
structureRow(rowH = 0) { return { listElement: [], rowH, status: 0, numM: 0, rowW: 0 }; }
|
||||
|
||||
// ===== Utility =====
|
||||
empty() {
|
||||
this.ele.innerHTML = '';
|
||||
this.listGrid?.clear();
|
||||
this.listRows = [];
|
||||
this.listCols = [];
|
||||
this.keyI = 1;
|
||||
this.isRender = false;
|
||||
this.gridWidth = 0;
|
||||
this.ele.style.height = '0px';
|
||||
this.gridWidth = this.ele.clientWidth;
|
||||
this.initCols();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ export default class AMenu extends window.AObject {
|
||||
|
||||
initNav(ele, type = "D") {
|
||||
let arr = ele.querySelectorAll(".nav-i.has-sub .nav-link, .navmain > .nav-i:not(.has-sub) a");
|
||||
console.log(arr);
|
||||
Array.from(arr).forEach(el => {
|
||||
var f = function (evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
@ -771,7 +771,9 @@ class AApp extends window.AObject {
|
||||
loadedPage() {
|
||||
this.metaPage = document.head.querySelector("meta[name=idPage]");
|
||||
if (this.metaPage != null && this.isLoadedLayout) {
|
||||
this.cachePage.getInf(window.location.href, ((tmp) => {
|
||||
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]();
|
||||
@ -918,11 +920,7 @@ class CacheManager {
|
||||
}
|
||||
|
||||
// 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 = "cahce") {
|
||||
@ -972,7 +970,7 @@ class CacheManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
// this.flexPages.insert(url, { "linkID": idN, "info": info });
|
||||
this.flexPages.insert(url, { "linkID": idN, "info": info });
|
||||
//this.storage.set(idN, htmlData, function () { }, type = "flex");
|
||||
if (this.countFP > 5) {
|
||||
this.countFP = 0;
|
||||
|
||||
Reference in New Issue
Block a user