Bab 3: UI Events
Detail interaksi user: mouse, keyboard, dan scroll — event yang paling sering kamu pakai sehari-hari.
3.1 Mouse Events
Mouse itu kayak jari kamu di layar sentuh — bisa klik, double-klik, tekan tahan, dan gerak. Browser melacak semua gerakan ini dan memberitahu kamu lewat event.
Jenis Mouse Events
| Event | Kapan |
|---|---|
click | Klik kiri (mousedown + mouseup di elemen yang sama) |
dblclick | Double-click |
mousedown | Tombol mouse ditekan |
mouseup | Tombol mouse dilepas |
contextmenu | Klik kanan (atau menu key) |
Urutan Event
Satu klik sebenarnya menghasilkan beberapa event berurutan:
mousedown → mouseup → click
Double-click:
mousedown → mouseup → click → mousedown → mouseup → click → dblclick
Tombol Mouse: event.button
elem.addEventListener("mousedown", function(event) {
switch (event.button) {
case 0: console.log("Kiri"); break;
case 1: console.log("Tengah (scroll wheel)"); break;
case 2: console.log("Kanan"); break;
}
});Modifier Keys
Cek apakah user menekan Shift/Ctrl/Alt/Meta saat klik:
elem.addEventListener("click", function(event) {
if (event.shiftKey) console.log("Shift + Click");
if (event.ctrlKey) console.log("Ctrl + Click");
if (event.altKey) console.log("Alt + Click");
if (event.metaKey) console.log("Cmd/Win + Click");
// Contoh: Ctrl+Click untuk buka di tab baru
if (event.ctrlKey || event.metaKey) {
// Biarkan browser handle (buka tab baru)
return;
}
event.preventDefault();
// Handle klik biasa...
});Koordinat Mouse
elem.addEventListener("click", function(event) {
// Relatif terhadap viewport (layar)
console.log("Client:", event.clientX, event.clientY);
// Relatif terhadap dokumen (termasuk scroll)
console.log("Page:", event.pageX, event.pageY);
});Mencegah Seleksi Teks saat Double-Click
elem.addEventListener("mousedown", function(event) {
if (event.detail > 1) { // detail = jumlah klik berturut
event.preventDefault(); // Cegah seleksi teks
}
});
// Atau via CSS:
// user-select: none;// 1. click event HANYA untuk tombol kiri
// Untuk kanan → pakai contextmenu
// Untuk semua tombol → pakai mousedown/mouseup
// 2. Jangan disable contextmenu di seluruh halaman
// User butuh copy-paste!
// 3. Mobile tidak punya mouse events yang sama
// touchstart/touchend berbeda dari mousedown/mouseup
// Tapi click tetap bekerja di mobile (dengan delay ~300ms)🎯 Challenge
// Buat "drawing canvas" sederhana:
// 1. Buat <div> besar (500x300px, border)
// 2. mousedown → mulai "menggambar" (set flag isDrawing = true)
// 3. mousemove + isDrawing → buat <div> kecil (5x5px, background hitam)
// di posisi mouse (pakai event.clientX/Y + getBoundingClientRect)
// 4. mouseup → berhenti menggambar
// Hint: position:relative pada canvas, position:absolute pada titik3.2 Pergerakan Mouse: mouseover/out, mouseenter/leave
Bayangin kamu punya sensor pintu:
- mouseover/mouseout = sensor yang juga deteksi gerakan di DALAM ruangan (antar furniture)
- mouseenter/mouseleave = sensor yang hanya deteksi masuk/keluar ruangan
mouseover dan mouseout
elem.addEventListener("mouseover", function(event) {
// Mouse MASUK elemen (atau child-nya)
console.log("Masuk:", event.target.tagName);
console.log("Dari:", event.relatedTarget?.tagName); // Elemen sebelumnya
});
elem.addEventListener("mouseout", function(event) {
// Mouse KELUAR elemen (atau pindah ke child)
console.log("Keluar:", event.target.tagName);
console.log("Ke:", event.relatedTarget?.tagName); // Elemen tujuan
});Masalah: Event Berlebihan
<div id="parent">
<span>Teks</span>
</div>Dengan mouseover/mouseout, pindah dari <div> ke <span> di dalamnya menghasilkan:
mouseoutdari<div>(keluar div)mouseoverpada<span>(masuk span)
Ini sering tidak diinginkan!
mouseenter dan mouseleave (Solusi)
elem.addEventListener("mouseenter", function(event) {
// Mouse masuk elemen — TIDAK trigger saat pindah antar child
this.style.background = "yellow";
});
elem.addEventListener("mouseleave", function(event) {
// Mouse keluar elemen — TIDAK trigger saat pindah antar child
this.style.background = "";
});Perbedaan Kunci
| mouseover/mouseout | mouseenter/mouseleave | |
|---|---|---|
| Trigger saat pindah ke child? | ✅ Ya | ❌ Tidak |
| Bubble? | ✅ Ya | ❌ Tidak |
| Event delegation? | ✅ Bisa | ❌ Tidak bisa |
Kapan Pakai Yang Mana?
// Pakai mouseenter/mouseleave untuk:
// - Highlight saat hover (paling umum)
// - Tooltip
// - Dropdown menu
// Pakai mouseover/mouseout untuk:
// - Event delegation (karena bubble)
// - Perlu tahu perpindahan antar child elementsContoh: Tooltip dengan mouseenter/mouseleave
const buttons = document.querySelectorAll("[data-tooltip]");
buttons.forEach(btn => {
btn.addEventListener("mouseenter", function() {
const tooltip = document.createElement("div");
tooltip.className = "tooltip";
tooltip.textContent = this.dataset.tooltip;
this.append(tooltip);
});
btn.addEventListener("mouseleave", function() {
this.querySelector(".tooltip")?.remove();
});
});Event Delegation dengan mouseover/mouseout
// Karena mouseenter tidak bubble, pakai mouseover untuk delegation
const table = document.getElementById("data-table");
let currentHighlight = null;
table.addEventListener("mouseover", function(event) {
const td = event.target.closest("td");
if (!td || !table.contains(td)) return;
if (td === currentHighlight) return; // Sudah di-highlight
if (currentHighlight) {
currentHighlight.style.background = ""; // Hapus highlight lama
}
currentHighlight = td;
td.style.background = "lightyellow"; // Highlight baru
});
table.addEventListener("mouseout", function(event) {
if (!currentHighlight) return;
// Cek apakah benar-benar keluar dari td
const relatedTarget = event.relatedTarget;
if (currentHighlight.contains(relatedTarget)) return; // Masih di dalam td
currentHighlight.style.background = "";
currentHighlight = null;
});// 1. relatedTarget bisa null (mouse dari luar window)
event.relatedTarget; // null kalau mouse dari luar browser
// 2. mouseenter/mouseleave TIDAK bubble
// Jadi tidak bisa pakai event delegation!
// Solusi: pakai mouseover/mouseout dengan logic tambahan
// 3. Cepat gerakkan mouse → bisa skip elemen
// Browser tidak menjamin mouseover di SETIAP elemen yang dilewati🎯 Challenge
<table id="schedule">
<tr><td>09:00</td><td>Meeting</td><td>Room A</td></tr>
<tr><td>10:00</td><td>Coding</td><td>Desk</td></tr>
<tr><td>12:00</td><td>Lunch</td><td>Cafeteria</td></tr>
</table>
<!-- Tulis JavaScript untuk:
1. Hover pada <tr> → seluruh row background jadi kuning
2. Pakai event delegation (mouseover/mouseout di <table>)
3. Pastikan highlight tidak "flicker" saat pindah antar <td> dalam row yang sama
-->3.3 Drag and Drop dengan Mouse Events
Drag and drop itu kayak angkat barang dan taruh di tempat lain. Prosesnya:
- Pegang (mousedown)
- Geser (mousemove)
- Lepas (mouseup)
Algoritma Dasar
const draggable = document.getElementById("ball");
draggable.addEventListener("mousedown", function(event) {
// Hitung offset (jarak mouse dari pojok kiri atas elemen)
const shiftX = event.clientX - draggable.getBoundingClientRect().left;
const shiftY = event.clientY - draggable.getBoundingClientRect().top;
// Pindahkan elemen ke body agar bisa bergerak bebas
draggable.style.position = "absolute";
draggable.style.zIndex = 1000;
document.body.append(draggable);
moveAt(event.pageX, event.pageY);
function moveAt(pageX, pageY) {
draggable.style.left = pageX - shiftX + "px";
draggable.style.top = pageY - shiftY + "px";
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// Gerak
document.addEventListener("mousemove", onMouseMove);
// Lepas
document.addEventListener("mouseup", function onMouseUp() {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
});
});
// Cegah drag bawaan browser
draggable.addEventListener("dragstart", function(event) {
event.preventDefault();
});Mendeteksi "Drop Zone"
Masalah: saat drag, elemen yang di-drag ada di bawah mouse, jadi elementFromPoint mengembalikan elemen itu sendiri.
Solusi: sembunyikan sebentar:
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
// Sembunyikan elemen yang di-drag
draggable.hidden = true;
// Cek elemen di bawah mouse
const elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// Tampilkan lagi
draggable.hidden = false;
if (!elemBelow) return;
// Cek apakah di atas drop zone
const dropZone = elemBelow.closest(".drop-zone");
if (dropZone) {
dropZone.classList.add("highlight");
}
}Contoh Lengkap: Sortable List
function makeSortable(list) {
let draggedItem = null;
list.addEventListener("mousedown", function(event) {
const item = event.target.closest("li");
if (!item) return;
draggedItem = item;
draggedItem.classList.add("dragging");
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
function onMove(event) {
if (!draggedItem) return;
// Cari item di bawah mouse
const items = [...list.querySelectorAll("li:not(.dragging)")];
const nextItem = items.find(item => {
const rect = item.getBoundingClientRect();
return event.clientY < rect.top + rect.height / 2;
});
// Pindahkan
if (nextItem) {
list.insertBefore(draggedItem, nextItem);
} else {
list.append(draggedItem);
}
}
function onUp() {
if (draggedItem) {
draggedItem.classList.remove("dragging");
draggedItem = null;
}
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
}
}// 1. HARUS preventDefault pada dragstart
elem.ondragstart = () => false; // Cegah drag bawaan browser
// 2. mousemove listener di DOCUMENT, bukan elemen
// Karena mouse bisa bergerak keluar elemen saat drag cepat
// 3. Jangan lupa cleanup (removeEventListener) saat mouseup
// 4. Performa: mousemove fire SANGAT sering
// Gunakan requestAnimationFrame kalau perlu🎯 Challenge
// Buat slider sederhana:
// 1. Buat track (div panjang horizontal, background abu-abu)
// 2. Buat thumb (div kecil bulat di atas track)
// 3. Drag thumb → hanya bisa gerak horizontal (dalam track)
// 4. Tampilkan nilai 0-100 berdasarkan posisi thumb
// Hint: clamp posisi antara 0 dan track.offsetWidth3.4 Pointer Events
Dulu ada mouse events dan touch events — dua sistem terpisah. Pointer events itu penyatuan — satu API untuk mouse, touch, dan stylus. Kayak adapter universal yang cocok untuk semua colokan.
Kenapa Pointer Events?
| Device | Event Lama | Pointer Event |
|---|---|---|
| Mouse | mousedown | pointerdown |
| Touch | touchstart | pointerdown |
| Pen/Stylus | — | pointerdown |
Satu kode untuk semua device!
Mapping Mouse → Pointer
| Mouse Event | Pointer Event |
|---|---|
| mousedown | pointerdown |
| mouseup | pointerup |
| mousemove | pointermove |
| mouseenter | pointerenter |
| mouseleave | pointerleave |
| mouseover | pointerover |
| mouseout | pointerout |
Properti Tambahan
elem.addEventListener("pointerdown", function(event) {
console.log(event.pointerId); // ID unik pointer (untuk multi-touch)
console.log(event.pointerType); // "mouse", "touch", atau "pen"
console.log(event.width); // Lebar area kontak
console.log(event.height); // Tinggi area kontak
console.log(event.pressure); // Tekanan (0-1, untuk stylus)
console.log(event.isPrimary); // true = pointer utama
});Pointer Capture
"Kunci" semua event pointer ke satu elemen (berguna untuk drag):
elem.addEventListener("pointerdown", function(event) {
// Semua pointermove/pointerup akan ke elem ini,
// meskipun mouse di luar elemen
elem.setPointerCapture(event.pointerId);
});
elem.addEventListener("pointermove", function(event) {
// Selalu diterima selama capture aktif
console.log(event.clientX, event.clientY);
});
elem.addEventListener("pointerup", function(event) {
// Capture otomatis dilepas saat pointerup
console.log("Selesai drag");
});
// Event saat capture dimulai/berakhir
elem.addEventListener("gotpointercapture", () => console.log("Capture dimulai"));
elem.addEventListener("lostpointercapture", () => console.log("Capture berakhir"));Drag dengan Pointer Events (Lebih Simpel!)
const slider = document.getElementById("thumb");
slider.addEventListener("pointerdown", function(event) {
slider.setPointerCapture(event.pointerId);
// Tidak perlu document.addEventListener!
});
slider.addEventListener("pointermove", function(event) {
// Otomatis diterima karena capture
slider.style.left = event.clientX + "px";
});
// Tidak perlu cleanup manual — capture otomatis lepas saat pointeruptouch-action CSS
/* Cegah browser handle touch (scroll, zoom) pada elemen tertentu */
.draggable {
touch-action: none; /* Kita yang handle semua touch */
}
.horizontal-slider {
touch-action: pan-y; /* Browser handle scroll vertikal, kita handle horizontal */
}// 1. Pointer events MENGGANTIKAN mouse events
// Kalau pakai pointer events, jangan campur dengan mouse events
// 2. touch-action HARUS di-set di CSS
// Tanpa ini, browser bisa "steal" touch untuk scroll/zoom
// 3. Browser masih fire mouse events setelah touch (untuk kompatibilitas)
// Urutan: pointerdown → touchstart → mousedown → click
// 4. setPointerCapture harus di dalam pointerdown handler🎯 Challenge
// Refactor challenge drag-and-drop dari sub-bab 3.3 menggunakan Pointer Events:
// 1. Ganti mousedown → pointerdown
// 2. Gunakan setPointerCapture (tidak perlu document listener)
// 3. Tambahkan touch-action: none di CSS
// 4. Test: harus bekerja di mouse DAN touch device3.5 Keyboard Events
Keyboard events itu kayak mesin ketik — setiap tombol yang ditekan dan dilepas menghasilkan sinyal. Kamu bisa menangkap sinyal ini untuk membuat shortcut, game controls, atau validasi input.
Dua Event Utama
document.addEventListener("keydown", function(event) {
// Tombol DITEKAN (fire berulang kalau ditahan!)
console.log("Key down:", event.key);
});
document.addEventListener("keyup", function(event) {
// Tombol DILEPAS
console.log("Key up:", event.key);
});event.key vs event.code
document.addEventListener("keydown", function(event) {
console.log("key:", event.key); // Karakter yang dihasilkan: "a", "A", "Enter"
console.log("code:", event.code); // Tombol fisik: "KeyA", "Enter", "ShiftLeft"
});| Tombol | event.key | event.code |
|---|---|---|
| A (tanpa shift) | "a" | "KeyA" |
| A (dengan shift) | "A" | "KeyA" |
| Enter | "Enter" | "Enter" |
| Shift kiri | "Shift" | "ShiftLeft" |
| Angka 1 (atas) | "1" | "Digit1" |
| Angka 1 (numpad) | "1" | "Numpad1" |
Kapan pakai yang mana?
event.key→ untuk input teks, shortcut yang bergantung pada karakterevent.code→ untuk game controls, shortcut yang bergantung pada posisi tombol
Modifier Keys
document.addEventListener("keydown", function(event) {
// Ctrl+S (Save)
if (event.ctrlKey && event.key === "s") {
event.preventDefault(); // Cegah browser save
console.log("Custom save!");
}
// Ctrl+Shift+Z (Redo)
if (event.ctrlKey && event.shiftKey && event.key === "Z") {
console.log("Redo!");
}
});Auto-repeat
Kalau tombol ditahan, keydown fire berulang-ulang:
document.addEventListener("keydown", function(event) {
if (event.repeat) {
console.log("Tombol ditahan (repeat)");
} else {
console.log("Tekan pertama");
}
});Contoh: Keyboard Shortcut System
const shortcuts = {
"ctrl+s": () => { console.log("Save"); },
"ctrl+z": () => { console.log("Undo"); },
"ctrl+shift+z": () => { console.log("Redo"); },
"escape": () => { console.log("Close modal"); }
};
document.addEventListener("keydown", function(event) {
let combo = "";
if (event.ctrlKey) combo += "ctrl+";
if (event.shiftKey) combo += "shift+";
if (event.altKey) combo += "alt+";
combo += event.key.toLowerCase();
if (shortcuts[combo]) {
event.preventDefault();
shortcuts[combo]();
}
});// 1. keydown di input → karakter BELUM masuk ke input
// Pakai "input" event kalau mau baca nilai setelah ketik
// 2. Beberapa kombinasi di-"curi" browser/OS
// Ctrl+T (tab baru), Ctrl+W (tutup tab) → tidak bisa dicegah
// 3. event.keyCode DEPRECATED — jangan pakai
// Pakai event.key atau event.code
// 4. Layout keyboard berbeda → event.code lebih reliable untuk posisi
// Di keyboard AZERTY, "KeyA" menghasilkan "q" (bukan "a")
// 5. Mobile keyboard: keydown/keyup tidak reliable
// Pakai "input" event untuk mobile🎯 Challenge
// Buat "typing game":
// 1. Tampilkan huruf random di layar (A-Z)
// 2. User harus tekan huruf yang benar
// 3. Kalau benar → skor +1, tampilkan huruf baru
// 4. Kalau salah → skor -1, warna merah sebentar
// 5. Timer 30 detik, tampilkan skor akhir
// Hint: event.key, Math.random(), setTimeout3.6 Scrolling
Scroll event itu kayak sensor di eskalator — setiap kali ada pergerakan, sensor memberitahu. Berguna untuk: lazy loading gambar, infinite scroll, "back to top" button, animasi saat scroll.
Mendengarkan Scroll
window.addEventListener("scroll", function() {
console.log("Scroll position:", window.pageYOffset);
});
// Untuk elemen tertentu (yang punya overflow:auto/scroll)
const box = document.getElementById("scroll-box");
box.addEventListener("scroll", function() {
console.log("Box scroll:", box.scrollTop);
});Contoh: Show/Hide Header saat Scroll
let lastScroll = 0;
const header = document.querySelector("header");
window.addEventListener("scroll", function() {
const currentScroll = window.pageYOffset;
if (currentScroll > lastScroll && currentScroll > 100) {
// Scroll ke bawah & sudah lewat 100px → sembunyikan header
header.style.transform = "translateY(-100%)";
} else {
// Scroll ke atas → tampilkan header
header.style.transform = "translateY(0)";
}
lastScroll = currentScroll;
});Contoh: Infinite Scroll
window.addEventListener("scroll", function() {
// Cek apakah sudah dekat bawah halaman
const nearBottom =
window.innerHeight + window.pageYOffset >=
document.body.offsetHeight - 200; // 200px sebelum bawah
if (nearBottom) {
loadMoreContent(); // Muat konten tambahan
}
});Contoh: Lazy Load Images
function lazyLoad() {
const images = document.querySelectorAll("img[data-src]");
images.forEach(img => {
const rect = img.getBoundingClientRect();
// Kalau gambar sudah terlihat di viewport
if (rect.top < window.innerHeight + 200) {
img.src = img.dataset.src; // Load gambar
img.removeAttribute("data-src"); // Hapus marker
}
});
}
window.addEventListener("scroll", lazyLoad);
lazyLoad(); // Jalankan sekali saat loadThrottling Scroll Event
Scroll event fire SANGAT sering (bisa 60x/detik). Perlu di-throttle:
// Throttle: jalankan maksimal 1x per 100ms
let isThrottled = false;
window.addEventListener("scroll", function() {
if (isThrottled) return;
isThrottled = true;
// Kerjakan sesuatu
console.log("Scroll handled:", window.pageYOffset);
setTimeout(() => {
isThrottled = false;
}, 100);
});
// Atau pakai requestAnimationFrame (lebih smooth)
let ticking = false;
window.addEventListener("scroll", function() {
if (!ticking) {
requestAnimationFrame(function() {
handleScroll();
ticking = false;
});
ticking = true;
}
});Mencegah Scroll
// preventDefault pada wheel event
elem.addEventListener("wheel", function(event) {
event.preventDefault(); // Cegah scroll
}, { passive: false }); // HARUS passive:false untuk bisa preventDefault
// ⚠️ Jangan cegah scroll di seluruh halaman tanpa alasan kuat!Modern Alternative: Intersection Observer
// Lebih efisien dari scroll event untuk lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img); // Berhenti observe setelah load
}
});
}, {
rootMargin: "200px" // Mulai load 200px sebelum terlihat
});
document.querySelectorAll("img[data-src]").forEach(img => {
observer.observe(img);
});// 1. Scroll event fire SANGAT sering — SELALU throttle!
// Tanpa throttle → performa jelek, halaman lag
// 2. passive: true (default di beberapa browser untuk scroll/touch)
// Artinya: browser TIDAK menunggu handler selesai sebelum scroll
// Kalau mau preventDefault → harus { passive: false }
// 3. Scroll position bisa desimal di beberapa browser
// Gunakan Math.round() kalau perlu integer
// 4. Intersection Observer lebih baik untuk kebanyakan use case
// Pakai scroll event hanya kalau perlu posisi scroll exact🎯 Challenge
// Buat "progress bar" yang menunjukkan seberapa jauh user sudah scroll:
// 1. Buat <div> fixed di atas halaman (height: 4px, background: blue)
// 2. Lebar berubah dari 0% sampai 100% sesuai scroll position
// 3. Throttle dengan requestAnimationFrame
// 4. Formula: scrolled% = pageYOffset / (scrollHeight - clientHeight) * 100
// 5. Buat halaman panjang (banyak paragraf) untuk testingRingkasan Bab 3
| Sub-bab | Inti |
|---|---|
| Mouse Events | click, mousedown/up, button, modifier keys, koordinat |
| Mouse Movement | mouseover/out (bubble) vs mouseenter/leave (tidak bubble) |
| Drag and Drop | mousedown → mousemove → mouseup, elementFromPoint |
| Pointer Events | Unified API (mouse+touch+pen), setPointerCapture |
| Keyboard | keydown/keyup, event.key (karakter) vs event.code (fisik) |
| Scrolling | Throttle!, Intersection Observer untuk lazy load |
Prinsip utama:
- Pointer events = masa depan (gantikan mouse events)
- Selalu throttle scroll events
event.keyuntuk shortcut,event.codeuntuk game- Intersection Observer > scroll event untuk visibility detection
Next: Bab 4 — Forms & Controls (input, validasi, focus, submit)
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.