Tooltip kustom premium via Alpine directive x-tooltip
- Directive global di app.js: satu tooltip singleton di body, posisi dari
getBoundingClientRect (fixed, tidak terpotong overflow-hidden), animasi
fade + slide halus, delay 120ms anti-kedip, fallback ke bawah jika di
tepi atas viewport, clamp ke viewport
- Tooltip gelap glass: bg-slate-900/95 + ring + shadow + backdrop-blur
- Badge kalender pelanggaran ('X pelanggaran') & presensi ('X siswa, Y
alpha') beralih dari :title native ke x-tooltip
- Sembunyi otomatis saat scroll/resize/klik
This commit is contained in:
@@ -4,4 +4,87 @@ import Alpine from 'alpinejs';
|
|||||||
|
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* x-tooltip — tooltip kustom premium (Alpine directive)
|
||||||
|
*
|
||||||
|
* Pemakaian: <span x-tooltip="'teks' atau ekspresi">...</span>
|
||||||
|
* Satu elemen tooltip global di <body>, posisi dihitung dari bounding rect
|
||||||
|
* (fixed), animasi fade + slide halus. Tidak terpotong elemen overflow-hidden.
|
||||||
|
*/
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
let sharedTip = null;
|
||||||
|
let showTimer = null;
|
||||||
|
let activeEl = null;
|
||||||
|
|
||||||
|
const getTip = () => {
|
||||||
|
if (sharedTip) return sharedTip;
|
||||||
|
sharedTip = document.createElement('div');
|
||||||
|
sharedTip.className =
|
||||||
|
'pointer-events-none fixed z-[70] px-2.5 py-1.5 rounded-lg bg-slate-900/95 text-white text-[11px] font-semibold shadow-xl ring-1 ring-white/10 backdrop-blur-sm whitespace-nowrap transition-all duration-150 opacity-0 translate-y-1';
|
||||||
|
sharedTip.style.display = 'none';
|
||||||
|
document.body.appendChild(sharedTip);
|
||||||
|
return sharedTip;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
window.clearTimeout(showTimer);
|
||||||
|
if (!sharedTip) return;
|
||||||
|
sharedTip.classList.add('opacity-0', 'translate-y-1');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (sharedTip) sharedTip.style.display = 'none';
|
||||||
|
}, 150);
|
||||||
|
activeEl = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const show = (el, text) => {
|
||||||
|
if (!text) return;
|
||||||
|
const tip = getTip();
|
||||||
|
tip.textContent = text;
|
||||||
|
tip.style.display = 'block';
|
||||||
|
tip.classList.add('opacity-0', 'translate-y-1');
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const tw = tip.offsetWidth;
|
||||||
|
const th = tip.offsetHeight;
|
||||||
|
let x = Math.round(r.left + r.width / 2 - tw / 2);
|
||||||
|
let y = r.top - th - 8;
|
||||||
|
if (y < 8) y = r.bottom + 8; // fallback: tampil di bawah elemen
|
||||||
|
x = Math.max(8, Math.min(x, window.innerWidth - tw - 8));
|
||||||
|
tip.style.left = x + 'px';
|
||||||
|
tip.style.top = y + 'px';
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
tip.classList.remove('opacity-0', 'translate-y-1');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Alpine.directive('tooltip', (el, { expression }) => {
|
||||||
|
const getText = () => {
|
||||||
|
try {
|
||||||
|
return Alpine.evaluate(el, expression);
|
||||||
|
} catch {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('mouseenter', () => {
|
||||||
|
window.clearTimeout(showTimer);
|
||||||
|
const text = getText();
|
||||||
|
activeEl = el;
|
||||||
|
// delay kecil agar tidak berkedip saat hover lintas elemen
|
||||||
|
showTimer = window.setTimeout(() => {
|
||||||
|
if (activeEl === el) show(el, text);
|
||||||
|
}, 120);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('mouseleave', hide);
|
||||||
|
el.addEventListener('click', hide);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sembunyikan saat halaman digulir/di-klik (posisi berubah / sel kalender re-render)
|
||||||
|
window.addEventListener('scroll', hide, true);
|
||||||
|
window.addEventListener('resize', hide);
|
||||||
|
document.addEventListener('click', hide);
|
||||||
|
});
|
||||||
|
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
<template x-if="day.total > 0 && day.isCurrentMonth">
|
<template x-if="day.total > 0 && day.isCurrentMonth">
|
||||||
<span class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
|
<span class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
|
||||||
:class="day.alpha > 0 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : 'bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-emerald-500/30'"
|
:class="day.alpha > 0 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : 'bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-emerald-500/30'"
|
||||||
:title="day.total + ' siswa, ' + day.alpha + ' alpha'"
|
x-tooltip="day.total + ' siswa, ' + day.alpha + ' alpha'"
|
||||||
x-text="day.total">
|
x-text="day.total">
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
<a :href="'{{ route('violations.index') }}?date_from=' + day.dateStr + '&date_to=' + day.dateStr"
|
<a :href="'{{ route('violations.index') }}?date_from=' + day.dateStr + '&date_to=' + day.dateStr"
|
||||||
class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
|
class="absolute bottom-1.5 right-1.5 inline-flex items-center justify-center min-w-[28px] h-[28px] text-xs font-extrabold text-white rounded-full shadow-lg transition-transform hover:scale-110"
|
||||||
:class="day.count >= 3 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : (day.count >= 2 ? 'bg-gradient-to-br from-orange-400 to-orange-500 shadow-orange-400/30' : 'bg-gradient-to-br from-blue-400 to-blue-500 shadow-blue-400/30')"
|
:class="day.count >= 3 ? 'bg-gradient-to-br from-red-500 to-rose-600 shadow-red-500/30' : (day.count >= 2 ? 'bg-gradient-to-br from-orange-400 to-orange-500 shadow-orange-400/30' : 'bg-gradient-to-br from-blue-400 to-blue-500 shadow-blue-400/30')"
|
||||||
:title="day.count + ' pelanggaran'"
|
x-tooltip="day.count + ' pelanggaran'"
|
||||||
x-text="day.count">
|
x-text="day.count">
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user