Le Bruni
Ô long Sữa Gạo rang
Vị nồng ấm của gạo rang kết hợp với vị trà ô long đặc sản đậm đà
Thức uống này được phục vụ với hai kích cỡ
/* Tư Trà — Price flip animation (scoped to your button markup) */
.tt-price-anim .tu-tra-live-price.tt-price-flip{
display:inline-block;
position:relative; /* anchor overlay */
vertical-align:baseline;
}
/* overlay wrapper injected only during the flip */
.tt-price-anim .tu-tra-live-price .ttpf-wrap{
position:absolute;
inset:0; /* left:0; top:0; right:0; bottom:0 */
overflow:hidden;
pointer-events:none;
}
/* two lines that slide */
.tt-price-anim .tu-tra-live-price .ttpf-line{
position:absolute;
left:0; right:0;
will-change:transform, opacity;
transform:translateY(0);
opacity:1;
transition:transform 360ms cubic-bezier(.22,.61,.36,1), opacity 360ms linear;
text-align:inherit;
white-space:nowrap;
}
/* enter positions */
.tt-price-anim .tu-tra-live-price .ttpf-line.new-up{ transform:translateY(100%); } /* price ↑ enters from bottom */
.tt-price-anim .tu-tra-live-price .ttpf-line.new-dn{ transform:translateY(-100%); } /* price ↓ enters from top */
/* phase classes on wrapper */
.tt-price-anim .tu-tra-live-price .ttpf-anim-up .old{ transform:translateY(-100%); opacity:0; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-up .new{ transform:translateY(0%); opacity:1; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-dn .old{ transform:translateY(100%); opacity:0; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-dn .new{ transform:translateY(0%); opacity:1; }
(function(){
// Host (.tt-price-anim) → we bind/rebind ONLY inside these
var HOST_SEL = '.tt-price-anim';
var PRICE_SEL = '.tu-tra-live-price, #tu-tra-live-price, [data-tu-tra-live-price], [data-dynamic-tag="tu_tra_live_price"], [data-brx-bind*="tu_tra_live_price"], [data-brx-content*="tu_tra_live_price"]';
function parsePrice(text){
if(!text) return null;
var t = String(text).replace(/\u00A0/g,' ').trim();
if(!t || /không có/i.test(t)) return null;
var digits = t.replace(/[^0-9]/g,'');
if(!digits) return null;
var n = parseInt(digits,10);
return Number.isFinite(n) ? n : null;
}
function flipOnce(el, oldText, newText){
if(oldText === newText) return;
if(el.__ttpfBusy) { el.__ttpfPrev = newText; return; }
el.__ttpfBusy = true;
var wrap = document.createElement('span');
wrap.className = 'ttpf-wrap';
var lineOld = document.createElement('span');
var lineNew = document.createElement('span');
lineOld.className = 'ttpf-line old';
lineNew.className = 'ttpf-line new';
var oldN = parsePrice(oldText);
var newN = parsePrice(newText);
var up = true;
if(oldN !== null && newN !== null) up = newN > oldN;
lineNew.classList.add(up ? 'new-up' : 'new-dn');
lineOld.textContent = oldText;
lineNew.textContent = newText;
// lock width/height to avoid jiggle during flip
var rect = el.getBoundingClientRect();
if(rect.width) el.style.minWidth = rect.width + 'px';
if(rect.height) el.style.minHeight = rect.height + 'px';
wrap.appendChild(lineOld);
wrap.appendChild(lineNew);
el.appendChild(wrap);
// set final text underneath (so DOM is correct during/after anim)
el.textContent = newText;
// kick animation next frame
requestAnimationFrame(function(){
wrap.classList.add(up ? 'ttpf-anim-up' : 'ttpf-anim-dn');
});
// cleanup
var cleaned = false;
function done(){
if(cleaned) return; cleaned = true;
if(wrap.parentNode) wrap.parentNode.removeChild(wrap);
el.style.minWidth = el.style.minHeight = '';
el.__ttpfPrev = newText;
el.__ttpfBusy = false;
}
wrap.addEventListener('transitionend', done, { once:true });
setTimeout(done, 650); // safety
}
function bindPrice(el){
if(!el) return;
if(el.__ttpfBound) return;
el.__ttpfBound = true;
el.__ttpfPrev = (el.textContent || '').trim();
el.__ttpfBusy = false;
el.classList.add('tt-price-flip');
// Observe this price span for text changes; coalesce via RAF
var mo = new MutationObserver(function(){
if(!el.isConnected){ try{mo.disconnect();}catch(e){} return; }
if(el.__ttpfRaf) cancelAnimationFrame(el.__ttpfRaf);
el.__ttpfRaf = requestAnimationFrame(function(){
var curr = (el.textContent || '').trim();
var prev = el.__ttpfPrev || '';
if(curr !== prev) flipOnce(el, prev, curr);
});
});
try{
mo.observe(el, { characterData:true, childList:true, subtree:true });
el.__ttpfMO = mo;
}catch(e){ /* ignore */ }
}
function bindHost(host){
if(!host || !host.isConnected) return;
// (re)bind price element inside this host
var priceEl = host.querySelector(PRICE_SEL);
if(priceEl) bindPrice(priceEl);
// host-level observer: if the price span is replaced, bind again
if(host.__ttpfHostMO) return;
var hostMO = new MutationObserver(function(muts){
// only react if a price element appears or is replaced inside host
if(!host.isConnected){ try{hostMO.disconnect();}catch(e){} return; }
var el = host.querySelector(PRICE_SEL);
if(el && !el.__ttpfBound) bindPrice(el);
});
try{
hostMO.observe(host, { childList:true, subtree:true });
host.__ttpfHostMO = hostMO;
}catch(e){ /* ignore */ }
}
function initAll(){
document.querySelectorAll(HOST_SEL).forEach(bindHost);
}
// Initial + late load safety
if(document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', initAll, { once:true });
} else {
initAll();
}
window.addEventListener('load', initAll, { once:true });
// Optional: small, filtered page observer to catch hosts added later by Bricks
var pageMO = new MutationObserver(function(muts){
for (var i=0;i<muts.length;i++){
var m = muts[i];
for (var j=0;j<m.addedNodes.length;j++){
var n = m.addedNodes[j];
if(!(n instanceof Element)) continue;
if(n.matches && n.matches(HOST_SEL)) bindHost(n);
var sub = n.querySelectorAll ? n.querySelectorAll(HOST_SEL) : [];
if(sub.length) sub.forEach(bindHost);
}
}
});
try{
pageMO.observe(document.body, { childList:true, subtree:true });
}catch(e){ /* ignore */ }
})();
- Loại tràÔ Long Thanh XuânNguồn gốcBảo Lộc, Lâm Đồng, Việt NamĐộ caoGiống tràĐộ lên menPhương pháp pha chếFrench pressThành phầnHương vịVị nồng ấm của gạo rang kết hợp với vị trà ô long đặc sản đậm đàMức độ ngọtCảm giác khi uốngKích cỡ
