update news & event 1

This commit is contained in:
2025-10-22 20:41:14 +07:00
parent 8f81545293
commit d4e91c7960
7 changed files with 379 additions and 80 deletions

View File

@ -0,0 +1,264 @@
// AElementFixed.snap-up-near-ref.js
// - Snap xuống (portal lên body) khi PROBE ra khỏi top viewport (như trước)
// - Khi lướt LÊN lại: nếu TARGET (đang ở trong CHA) tiến gần REF (±snapUpProximity quanh ref.bottom+unsnapThreshold) → SNAP ABSOLUTE
// - Unsnap khi PROBE vào lại viewport và |ref.top - target.top| > unsnapThreshold (giữ logic cũ)
// - Khi đang snap: nếu đáy TARGET chạm/vượt đáy CHA → trả về CHA & pin bottom:0
export default class AElementFixed extends window.AObject {
constructor(opts = {}) {
super();
this.o = Object.assign({
ref: null, // Element | selector
target: null, // Element | selector
breakpoint: 992, // < => mobile: unsnap
unsnapThreshold: 40, // px: ngưỡng unsnap và offset top khi snap
enableTargetBottomSnap: true, // snap nếu target ra đáy viewport (khi đang ở CHA)
gutterLeft: 20, // px: left khi pin bottom về CHA
snapUpProximity: 24 // px: khoảng "gần" để snap khi lướt lên lại
}, opts);
this.ref = typeof this.o.ref === 'string' ? document.querySelector(this.o.ref) : this.o.ref;
this.el = typeof this.o.target === 'string' ? document.querySelector(this.o.target) : this.o.target;
if (!this.ref || !this.el) throw new Error('[AElementFixed] Missing ref/target');
// state
this.snapped = false;
this._isPortaled = false;
this._hostParent = null; // CHA ban đầu
this._loopId = null; // rAF: loop khi probe trong viewport (check snap-up & unsnap)
this._snapLoopId = null; // rAF: monitor chạm đáy CHA khi đang snap
this._needIntroPose = true;// hiệu ứng intro khi snap
this._snapWidth = null; // lock width (px) khi portal
this._bind();
this._initObservers();
this._onResize();
}
update(opts = {}) {
Object.assign(this.o, opts);
this._stopInViewLoop();
this._stopSnapMonitor();
this._teardownObservers();
this._initObservers();
this._onResize();
}
destroy() {
this._stopInViewLoop();
this._stopSnapMonitor();
this._teardownObservers();
this._unsnapAbsolute();
window.removeEventListener('resize', this._onResize);
}
// ===== private =====
_bind() {
this._onResize = this._onResize.bind(this);
window.addEventListener('resize', this._onResize);
}
_onResize() {
const desktop = window.innerWidth >= this.o.breakpoint;
if (!desktop) {
this._stopInViewLoop();
this._stopSnapMonitor();
this._unsnapAbsolute();
}
}
_initObservers() {
// PROBE: đặt TRƯỚC target trong CHA để giữ vị trí flow
this.probe = document.createElement('div');
this.probe.setAttribute('data-aef-probe', '');
Object.assign(this.probe.style, { width: '1px', height: '1px', pointerEvents: 'none', opacity: 0 });
this.el.parentElement.insertBefore(this.probe, this.el);
// PROBE với root = viewport
this.ioProbeVP = new IntersectionObserver((entries) => {
for (const e of entries) {
// PROBE rời viewport phía TRÊN => SNAP ngay
if (!e.isIntersecting && e.boundingClientRect.top < 0) {
if (!this.snapped) this._snapAbsolute();
this._stopInViewLoop();
continue;
}
// PROBE vào viewport => bật loop để:
// - nếu đang snap: xét UNSNAP
// - nếu không snap: xét SNAP-UP khi target gần ref
if (e.isIntersecting) {
this._needIntroPose = true;
this._startInViewLoop();
}
}
}, { root: null, threshold: 0 });
this.ioProbeVP.observe(this.probe);
// (Tuỳ chọn) TARGET với root = viewport: out đáy viewport (khi ở CHA) → snap
if (this.o.enableTargetBottomSnap) {
this.ioTargetVP = new IntersectionObserver((entries) => {
for (const e of entries) {
if (this.snapped) continue;
// Bắt "đáy chạm đáy viewport" (vẫn intersect) để mượt hơn:
const vpBottom = e.rootBounds ? e.rootBounds.bottom : document.documentElement.clientHeight;
const EPS = 0.75;
if (e.isIntersecting) {
console.log(e.target.style.bottom);
if (e.target.style.bottom === '0px') {
this._startInViewLoop();
}
}
}
}, { root: null, threshold: [0, 1] });
this.ioTargetVP.observe(this.el);
}
}
_teardownObservers() {
if (this.ioProbeVP) { this.ioProbeVP.disconnect(); this.ioProbeVP = null; }
if (this.ioTargetVP) { this.ioTargetVP.disconnect(); this.ioTargetVP = null; }
if (this.probe && this.probe.parentNode) this.probe.parentNode.removeChild(this.probe);
this.probe = null;
}
// ===== Loop khi PROBE đang trong viewport: xét SNAP-UP & UNSNAP =====
_startInViewLoop() {
if (this._loopId) return;
const tick = () => {
const rr = this.ref.getBoundingClientRect();
const tr = this.el.getBoundingClientRect();
const anchor = rr.bottom + this.o.unsnapThreshold; // mốc bám khi snap
if (this.snapped) {
// UNSNAP: nếu lệch khỏi mốc quá ngưỡng (tránh dính khi user kéo mạnh)
const dy = Math.abs(tr.top - rr.top);
if (dy > this.o.unsnapThreshold) {
this._unsnapAbsolute(); // trả về CHA, giữ width
this._stopInViewLoop();
return;
}
} else {
// SNAP-UP khi lướt lên: target gần ref (quanh anchor)
const delta = Math.abs(tr.top - anchor);
if (delta <= this.o.snapUpProximity) {
this._snapAbsolute();
// sau snap, tiếp tục monitor chạm đáy CHA; loop này tự dừng khi ioProbe báo ra viewport
this._stopInViewLoop();
return;
}
}
this._loopId = requestAnimationFrame(tick);
};
this._loopId = requestAnimationFrame(tick);
}
_stopInViewLoop() {
if (this._loopId) { cancelAnimationFrame(this._loopId); this._loopId = null; }
}
// ===== Snap monitor: đang snap → nếu đáy target chạm/vượt đáy CHA => pin bottom:0 =====
_startSnapMonitor() {
if (this._snapLoopId) return;
const tickSnap = () => {
if (!this.snapped || !this._isPortaled || !this._hostParent) {
this._stopSnapMonitor();
return;
}
const pr = this._hostParent.getBoundingClientRect();
const er = this.el.getBoundingClientRect();
if (er.bottom >= pr.bottom) {
this._pinToHostBottom();
this._stopSnapMonitor();
return;
}
this._snapLoopId = requestAnimationFrame(tickSnap);
};
this._snapLoopId = requestAnimationFrame(tickSnap);
}
_stopSnapMonitor() {
if (this._snapLoopId) { cancelAnimationFrame(this._snapLoopId); this._snapLoopId = null; }
}
// ===== helpers =====
_portalToBody() {
if (this._isPortaled) return;
this._hostParent = this.el.parentNode;
// lock width trước khi portal để không nhảy layout
this._snapWidth = this.el.offsetWidth;
if (this._snapWidth && Number.isFinite(this._snapWidth)) {
this.el.style.width = this._snapWidth + 'px';
}
document.body.appendChild(this.el);
this._isPortaled = true;
}
// Trả về CHA: append vào host (đứng sau mọi thứ; nếu probe ở cuối thì tự sau probe)
_restoreToHostBeforeProbe() {
if (!this._isPortaled) return;
if (this._hostParent) this._hostParent.appendChild(this.el);
else document.body.appendChild(this.el); // fallback hiếm
this._isPortaled = false;
// GIỮ style.width để giữ bề ngang như yêu cầu
}
// Pin về CHA bottom:0 (absolute trong CHA), giữ width
_pinToHostBottom() {
this._restoreToHostBeforeProbe();
const host = this._hostParent || this.el.parentNode;
if (host && getComputedStyle(host).position === 'static') {
host.style.position = 'relative';
}
this.el.style.position = 'absolute';
this.el.style.left = this.o.gutterLeft + 'px';
this.el.style.top = '';
this.el.style.bottom = '0px';
this.snapped = false;
}
// ===== Snap / Unsnap =====
_snapAbsolute() {
if (this.snapped) return;
this.snapped = true;
// đo trước khi portal để giữ LEFT & intro pose
const tRectBefore = this.el.getBoundingClientRect();
const rRect = this.ref.getBoundingClientRect();
const pageLeft = Math.round(tRectBefore.left);
const pageTopNow = Math.round(tRectBefore.top + this.o.unsnapThreshold);
const pageTopFinal = Math.round(rRect.bottom + this.o.unsnapThreshold);
// portal lên body (đồng thời lock width)
this._portalToBody();
// intro pose → frame kế tiếp về top final
this.el.style.position = 'absolute';
this.el.style.left = pageLeft + 'px';
this.el.style.top = (this._needIntroPose ? pageTopNow : pageTopFinal) + 'px';
this.el.style.bottom = '';
if (this._needIntroPose) {
requestAnimationFrame(() => { this.el.style.top = pageTopFinal + 'px'; });
this._needIntroPose = false;
}
this._startSnapMonitor();
}
_unsnapAbsolute() {
if (!this.snapped) return;
this.snapped = false;
// không xoá width → giữ nguyên bề ngang
this.el.style.position = '';
this.el.style.left = '';
this.el.style.top = '';
this.el.style.bottom = '';
this._restoreToHostBeforeProbe();
}
}