IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging17/29-dars23 daqiqa
Mundarija (35)

Sichqoncha va touch hodisalari JavaScript'da: click, Pointer Events va setPointerCapture

Qisqacha: Oddiy bosish uchun click yetadi — u sichqoncha, barmoq va klaviatura bilan ham ishlaydi. Sudrash, chizish kabi harakatlar uchun Pointer Events (pointerdown, pointermove, pointerup) ishlatiladi: bitta kod sichqonchani ham, barmoqni ham, qalamni ham qamraydi. Barmoq elementdan chiqib ketsa ham kuzatish uchun setPointerCapture, brauzer aylantirishga "o'g'irlab" ketmasligi uchun CSS'da touch-action kerak.

Bu darsda

  • Bitta bosish ichida qaysi hodisalar va qaysi tartibda kelishini bilasiz: pointerdown → mousedown → ... → click.
  • mouseenter va mouseover farqini ko'rib, kerakligini tanlay olasiz.
  • Pointer Events bilan sichqoncha, barmoq va qalamga bitta kod yozasiz.
  • setPointerCapture va touch-action yordamida sudraladigan panel yasaysiz.
  • clientX, pageX, offsetX koordinatalarini farqlaysiz.

Oldin bilishingiz kerak: Listener opsiyalari: once, passive, signal, Element o'lchamlari va scroll, Event obyekti va standart harakatlar, Mobil qurilma tuzoqlari.

1. Nega bu kerak?

Aziz «Bahor» saytiga telefon uchun pastki panel qo'shdi: buyurtma tafsilotlari ekran pastidan chiqadi. Bunday panelni barmoq bilan pastga surib yopish CSS'da yo'q, bu JavaScript ishi (Modal oyna va skroll bloklash darsida shunday degan edik).

Aziz internetdan mousedown va mousemove bilan yozilgan kodni topdi. Kompyuterda hammasi ishladi. Telefonda esa panel barmoqqa ergashmadi: barmoq surilganda butun sahifa aylanib ketdi.

Sabab — qurilmalar har xil. Sichqoncha, barmoq, planshet qalami — har biri ekranga boshqacha "tegadi". Tarixan ularning har biri uchun alohida hodisalar bo'lgan. Bu dars ularni bitta tizimga yig'adi: qaysi hodisa qachon keladi, qaysini tanlash kerak va brauzer barmoq harakatini qachon o'ziga olib qo'yadi.

2. Bosish: bitta harakat — beshta hodisa

2.1 Bosish ichida nima bo'ladi

Siz sichqoncha tugmasini bosib qo'yib yuborasiz. Bu bitta harakatdek tuyuladi. Brauzer uchun esa bu bir necha hodisa: tugma pastga tushdi, tugma ko'tarildi va, ikkalasi bir element ustida bo'lgani uchun, "bosildi".

Tekshiramiz. Quyidagi oynada tugmaga beshta tinglovchi qo'yilgan, har biri hodisa obyekti turini — event.type ni — konsolga yozadi.

html
<button type="button" id="osh">Osh — 35 000 so'm</button>
<script>
  const osh = document.getElementById("osh");
  const turlar = [
    "pointerdown", "mousedown", "pointerup", "mouseup", "click",
  ];
  for (const tur of turlar) {
    osh.addEventListener(tur, (event) => {
      console.log(event.type);
    });
  }
</script>

Tugmani sichqoncha bilan bir marta bosing — konsolda besh qator ketma-ket chiqadi (sahifa ochilganda esa konsol bo'sh):

text
pointerdown
mousedown
pointerup
mouseup
click

Ikki oila ko'rinyapti. pointer... — yangi, umumiy oila (birozdan keyin ko'ramiz). mouse... — eski, faqat sichqoncha uchun oila. Brauzer ikkalasini ham yuboradi: eski kod buzilmasin deb. click esa eng oxirida keladi.

2.2 click — eng ishonchli tanlov

Qaysi hodisani tinglash kerak? Tugma, havola, kartochka uchun javob deyarli doim — click. Sababi:

  • click sichqoncha bilan ham, barmoq bilan ham keladi.
  • click klaviatura bilan ham keladi: tugmada Enter yoki bo'sh joy (Space) bosilsa. Buni Klaviatura hodisalari darsida sinab ko'ramiz.
  • mousedown esa faqat sichqoncha uchun. Unga yozilgan tugma Bobur kabi faqat klaviatura ishlatadigan mehmon uchun ishlamaydi (Klaviatura va fokus).

Hayotdan o'xshatish: choyxonada mehmon ofitsiantni qo'l ko'tarib, ovoz berib yoki stolga qo'ng'iroq chalib chaqirishi mumkin. Otabek uchun hammasining ma'nosi bitta — "mehmon chaqiryapti". click ham shunday: qanday bosilgani muhim emas, natija — "tanlandi".

2.3 Ikki marta bosish: detail va dblclick

click hodisasining detail xususiyati bor — ketma-ket nechanchi bosish ekanini aytadi. Ikki marta tez bossangiz, dblclick ham keladi:

html
<button type="button" id="tugma">Osh qo'shish</button>
<p>Savatda: <output id="soni">0</output> ta</p>
<script>
  const tugma = document.getElementById("tugma");
  const soni = document.getElementById("soni");
  let jami = 0;

  tugma.addEventListener("click", (event) => {
    jami++;
    soni.textContent = jami;
    console.log(`click, detail: ${event.detail}`);
  });
  tugma.addEventListener("dblclick", () => {
    console.log("dblclick");
  });
</script>

Tugmani ikki marta tez bosing (double-click). Natijaga qarang: savatda 2 ta osh, konsolda esa shu uch qator:

text
click, detail: 1
click, detail: 2
dblclick

Muhim saboq: dblclick ikki click ning o'rnini bosmaydi, ulardan keyin qo'shimcha keladi. Agar bitta elementda click "qo'shish", dblclick esa "o'chirish" qilsa, ikki marta bosgan mehmon avval ikki osh qo'shadi, keyin o'chiradi. Shuning uchun bitta elementga ikkala hodisaga har xil ish bermang.

dblclick ning yana bir kamchiligi bor: telefonda ikki marta tegish ko'pincha ekranni kattalashtiradi, klaviaturada esa "ikki marta bosish" umuman yo'q. Muhim ish faqat dblclick ga bog'lanmasin.

2.4 Qaysi tugma bosildi: button va contextmenu

Sichqonchaning chap, o'rta (g'ildirak) va o'ng tugmalari bor. Qaysi biri bosilganini event.button aytadi:

event.button Tugma Qaysi hodisa keladi
0 Chap click
1 O'rta (g'ildirak) auxclick
2 O'ng auxclick va contextmenu

O'ng tugma bosilganda brauzer o'z menyusini ochadi: "Nusxa olish", "Kodni ko'rish". Undan oldin contextmenu hodisasi keladi. Unda preventDefault() chaqirsangiz — standart harakat to'xtaydi, menyu ochilmaydi:

html
<p id="karta" style="padding: 16px; background: #fffaf0">
  Lag'mon — 28 000 so'm (o'ng tugma bilan bosing)
</p>
<script>
  const karta = document.getElementById("karta");
  karta.addEventListener("mousedown", (event) => {
    console.log(`tugma: ${event.button}`);
  });
  karta.addEventListener("contextmenu", (event) => {
    event.preventDefault();
    console.log("contextmenu — brauzer menyusi to'xtatildi");
  });
</script>

Kartani o'ng tugma bilan bosing: konsolda avval tugma: 2, keyin contextmenu — ... chiqadi, brauzer menyusi esa ochilmaydi. Chap tugma bilan bossangiz — faqat tugma: 0.

contextmenu faqat sichqonchadan kelmaydi. Klaviaturadagi menyu tugmasi va Shift+F10 ham shu hodisani yuboradi (tekshirdik). Telefonda esa uzoq bosib turish ham ko'pincha shu hodisani beradi.

Diqqat: Brauzer menyusini o'chirish — kamdan-kam to'g'ri qaror. Mehmon unda "Havolani yangi tabda ochish" yoki "Tarjima qilish" ni qidiradi. O'z menyungiz faqat jadval, xarita, rasm tahrirlagich kabi "ilova" ichida o'rinli — va undagi har ish oddiy tugma bilan ham bajarilsin.

Tekshirib ko'ring: Savatga qo'shish tugmasini mousedown ga yozsa nima yomon?

Javob

Uch muammo. Klaviatura bilan bosilganda mousedown kelmaydi — Bobur osh qo'sha olmaydi. Mehmon tugmani bosib, fikridan qaytib, sichqonchani chetga surib qo'yib yuborsa, click bo'lmaydi, lekin mousedown allaqachon ishlab bo'lgan. Va mousedown o'ng tugmada ham keladi. click da bu uchala muammo yo'q.

3. Ustiga kelish: mouseover va mouseenter

3.1 Ikki juft hodisa

Sichqoncha element ustiga kelganda va chiqib ketganda ikki xil juft hodisa bor:

  • mouseover / mouseout — bubble qiladi: ichki elementlarga kirib-chiqqanda ham keladi.
  • mouseenter / mouseleave — bubble qilmaydi: faqat elementning o'zi chegarasidan o'tilganda keladi.

Farqni bitta kartada ko'ramiz. Karta ichida narx span i bor:

html
<style>
  #karta { padding: 24px; background: #fffaf0; max-width: 260px; }
  #narx { background: #e8f1ec; padding: 8px; }
</style>
<div id="karta">Manti <span id="narx">30 000</span></div>
<script>
  const karta = document.getElementById("karta");
  const turlar = [
    "mouseover", "mouseout", "mouseenter", "mouseleave",
  ];
  for (const tur of turlar) {
    karta.addEventListener(tur, (event) => {
      console.log(`${event.type}: ${event.target.id}`);
    });
  }
</script>

Sichqonchani kartaga olib kiring, keyin narx ustiga, yana kartaga qaytaring va tashqariga chiqing. Konsolda shu sakkiz qator chiqadi (tekshirdik):

text
mouseover: karta
mouseenter: karta
mouseout: karta
mouseover: narx
mouseout: narx
mouseover: karta
mouseout: karta
mouseleave: karta

Qatorma-qator o'qing. mouseenter va mouseleave faqat bir marta keldi — kartaga kirganda va undan butunlay chiqqanda. mouseover/mouseout esa narxga o'tganda ham "kartadan chiqdi" (mouseout: karta) deb xabar berdi. Sichqoncha kartadan chiqmagan edi — u faqat ichki elementga o'tdi.

3.2 Qaysi birini tanlash kerak?

Vazifa Tanlov Nega
"Element ustida turibdimi?" mouseenter / mouseleave Ichki elementlar chalg'itmaydi
Ro'yxatdagi ko'p elementni bitta tinglovchi bilan kuzatish mouseover / mouseout Bubble qiladi — delegation uchun

Lekin birinchi savol boshqa: JavaScript umuman kerakmi? Faqat rang yoki soya o'zgarsa — bu CSS'ning :hover ishi (Pseudo-klasslar). JavaScript faqat mantiq kerak bo'lganda: masalan, sichqoncha xaritadagi filial ustiga kelganda uning ish vaqtini yuklash.

Diqqat: Telefonda "ustiga kelish" yo'q. Muhim narsa faqat hover'da ko'rinadigan bo'lsa (masalan, menyu faqat sichqoncha ustiga kelganda ochilsa), telefondagi mehmon uni hech qachon ko'rmaydi. Hover — qo'shimcha qulaylik, yagona yo'l emas.

4. Pointer Events: bitta API — barcha qurilmalar

4.1 Uch oila tarixi

Avval faqat sichqoncha bor edi va mouse... hodisalari yaratildi. Keyin sensorli telefonlar chiqdi va ular uchun alohida Touch Events (touchstart, touchmove, touchend) qo'shildi. Natijada sudraladigan har element uchun ikki marta kod yozish kerak bo'ldi.

Pointer Events (ko'rsatkich hodisalari) bu muammoni hal qiladi. Pointer (ko'rsatkich) — ekranga "tegadigan" har qanday narsa: sichqoncha kursori, barmoq yoki qalam. Hodisalar bitta: pointerdown, pointermove, pointerup. Qaysi qurilma ekanini esa event.pointerType aytadi.

Baseline ma'lumotiga ko'ra Pointer Events 2020-yil iyuldan beri barcha asosiy brauzerlarda bor (keng tarqalgan Baseline). Touch Events esa Baseline emas: kompyuterdagi Safari'da yo'q. Yangi kodda Pointer Events'ni tanlang.

mouse... pointer... Qachon
mousedown pointerdown Tugma bosildi yoki barmoq tegdi
mousemove pointermove Harakat
mouseup pointerup Qo'yib yuborildi
mouseenter / mouseleave pointerenter / pointerleave Ustiga keldi / chiqdi
— pointercancel Brauzer harakatni o'ziga oldi

Oxirgi qator yangi — uni «touch-action» bo'limida ko'ramiz.

4.2 Kim tegdi: pointerType, pointerId, isPrimary

html
<div id="maydon" style="padding: 32px; background: #e8f1ec">
  Shu yerga sichqoncha yoki barmoq bilan bosing
</div>
<script>
  const maydon = document.getElementById("maydon");
  maydon.addEventListener("pointerdown", (event) => {
    console.log(
      event.pointerType, event.pointerId, event.isPrimary,
    );
  });
</script>

Kompyuterda sichqoncha bilan bossangiz, konsolda mouse 1 true chiqadi. Telefonda barmoq bilan bossangiz — touch, boshqa raqam va true.

  • pointerType — "mouse", "touch" yoki "pen" (qalam).
  • pointerId — har ko'rsatkichning raqami. Ikki barmoq bilan tegsangiz, ikkalasining raqami har xil bo'ladi.
  • isPrimary — "asosiy" ko'rsatkichmi. Ikkinchi barmoq uchun false. Ko'p barmoqli harakat kerak bo'lmasa, faqat asosiysini ishlating.

Kompyuterda barmoqni sinash uchun DevTools'dagi qurilma rejimi bor: Ctrl+Shift+M. Unda sichqoncha bosishi "barmoq" sifatida yuboriladi va pointerType "touch" bo'ladi.

4.3 Barmoq bilan bosilganda tartib

Barmoq bilan bir marta tegsangiz, eski kodlar buzilmasligi uchun brauzer uchala oilani ham yuboradi. Biz DevTools'ning sensor rejimida tekshirib chiqdik — tartib shunday:

flowchart TD
  A["pointerdown"] --> B["touchstart"]
  B --> C["pointerup"]
  C --> D["touchend"]
  D --> E["mousedown"]
  E --> F["mouseup"]
  F --> G["click"]

Diagrammadan ikki xulosa. Birinchi: click bu yerda ham oxirida keladi, shuning uchun tugmalar uchun click barmoqda ham ishlaydi. Ikkinchi: mouse... hodisalari barmoq qo'yib yuborilgandan keyin keladi. Sudrash davomida esa ular umuman kelmaydi. Aziz kodi telefonda ishlamaganining sababi shu.

5. Koordinatalar: bosilgan nuqta qayerda?

Hodisa obyektida bosilgan nuqtaning koordinatalari bor — bir nechta "sanoq boshi" bilan:

Xususiyat Nimaga nisbatan Qachon kerak
clientX, clientY Oynaning ko'rinib turgan qismi Ko'pincha shu; getBoundingClientRect bilan birga
pageX, pageY Butun sahifa (aylantirilgan qismi bilan) Hujjatdagi aniq joy
offsetX, offsetY Hodisa nishoni (target) elementi Element ichida chizish
screenX, screenY Butun monitor Deyarli kerak emas

clientX va pageX farqini sahifani aylantirib ko'ramiz. Oyna balandligi qotirilgan, shuning uchun uni aylantirish mumkin:

html
<style>
  body { height: 900px; margin: 0; }
  #nishon {
    margin: 60px 0 0 40px; width: 200px; height: 120px;
    background: #fffaf0; border: 4px solid #2f6b3a;
  }
</style>
<div id="nishon">Shu yerga bosing</div>
<script>
  const nishon = document.getElementById("nishon");
  nishon.addEventListener("click", (event) => {
    console.log(`client: ${event.clientX}, ${event.clientY}`);
    console.log(`page: ${event.pageX}, ${event.pageY}`);
    console.log(event.pageY === event.clientY + window.scrollY);
  });
</script>

Avval aylantirmasdan bosing: client va page bir xil chiqadi. Endi oynani sal pastga aylantirib, yana bosing. clientY kichraydi (element ekranning tepasiga yaqinlashdi), pageY esa o'sha joyda o'zgarmaydi. Uchinchi qator har safar true chiqadi: sahifadagi joy = ekrandagi joy + aylantirilgan masofa. window.scrollY ni Element o'lchamlari va scroll darsida ko'rgansiz.

Sonlarning o'zi siz bosgan nuqtaga bog'liq — har kimda har xil.

offsetX da bitta tuzoq bor: u hodisa nishoniga nisbatan. Element ichida bola element bo'lsa va siz bolaga bossangiz, offsetX bolaga nisbatan hisoblanadi. Shuning uchun element ichidagi joy kerak bo'lsa, ishonchli formula — event.clientX - element.getBoundingClientRect().left. Uni pastdagi sudrash misolida ishlatamiz.

Tekshirib ko'ring: Sahifa 300 px pastga aylantirilgan. Mehmon ekranning eng tepasidan 20 px pastga bosdi. clientY va pageY nechaga teng?

Javob

clientY — 20 (oynaning ko'rinib turgan qismiga nisbatan). pageY — 320 (sahifa boshidan: 20 + 300). Aylantirilmagan sahifada ikkalasi teng bo'lardi.

6. Sudrash: setPointerCapture

6.1 Muammo: kursor elementdan "qochib" ketadi

Endi Aziz panelini quramiz. Panelning tepasida "tutqich" bor — uni ushlab pastga sursangiz, panel ham ergashadi. Oddiy reja: pointerdown da boshlanish nuqtasini eslab qolish, pointermove da panelni siljitish, pointerup da to'xtatish.

Muammo shundaki, tez sursangiz, kursor tutqichdan chiqib ketadi. pointermove va pointerup esa faqat element ustida bo'lganda keladi. Kursor chetga chiqdi — hodisalar to'xtadi. Mehmon tugmani qo'yib yuborganini panel bilmay qoladi va "yopishib" qoladi.

Biz buni tekshirdik: setPointerCapture siz kursor 300 px chetga chiqqanda faqat bitta pointermove keldi, pointerup esa umuman kelmadi.

6.2 Yechim: ko'rsatkichni "ushlab olish"

setPointerCapture(pointerId) — "shu ko'rsatkich qo'yib yuborilguncha uning barcha hodisalarini menga yubor" degani. Kursor qayerda bo'lishidan qat'i nazar, pointermove va pointerup shu elementga keladi. Qo'yib yuborilganda ushlash o'zi tugaydi.

Hayotdan o'xshatish: bozorda bolani qo'lidan ushlab yurasiz. Olomon ichida u bir qadam chetga chiqsa ham, qo'li sizning qo'lingizda — yo'qotmaysiz. Ushlamasangiz, birinchi burilishda ko'zdan yo'qoladi.

html
<style>
  #panel {
    position: fixed; inset-inline: 0; bottom: 0;
    background: #fffaf0; border-top: 2px solid #2f6b3a;
    padding: 8px 16px 24px; transition: transform 0.2s;
  }
  #tutqich {
    width: 60px; height: 24px; margin: 0 auto 8px;
    background: #2f6b3a; border-radius: 12px;
    touch-action: none; cursor: grab;
  }
  #panel.surilmoqda { transition: none; }
</style>
<p>Panelning yashil tutqichini pastga suring.</p>
<div id="panel">
  <div id="tutqich"></div>
  <p>Buyurtma: osh, ko'k choy — 40 000 so'm</p>
</div>
<script>
  const panel = document.getElementById("panel");
  const tutqich = document.getElementById("tutqich");
  let boshY = 0;

  tutqich.addEventListener("pointerdown", (event) => {
    boshY = event.clientY;
    tutqich.setPointerCapture(event.pointerId);
    panel.classList.add("surilmoqda");
  });

  tutqich.addEventListener("pointermove", (event) => {
    if (!tutqich.hasPointerCapture(event.pointerId)) {
      return;
    }
    const masofa = Math.max(0, event.clientY - boshY);
    panel.style.transform = `translateY(${masofa}px)`;
  });

  tutqich.addEventListener("pointerup", (event) => {
    const masofa = event.clientY - boshY;
    panel.classList.remove("surilmoqda");
    if (masofa > 80) {
      panel.hidden = true;
      console.log(`Panel yopildi (${masofa} px)`);
    } else {
      panel.style.transform = "";
      console.log("Joyiga qaytdi");
    }
  });
</script>

Tutqichni ushlab, 80 px dan ko'proq pastga suring va qo'yib yuboring — panel yo'qoladi, konsolda Panel yopildi (...) chiqadi. Ozgina surib qo'yib yuborsangiz — panel joyiga qaytadi va Joyiga qaytdi yoziladi. Kursor tutqichdan ancha chetga chiqib ketsa ham panel ergashaveradi — bu setPointerCapture ishi.

Endi "Tahrirlab ko'rish" bilan setPointerCapture qatorini o'chirib, tez suring. Kursor tutqichdan chiqishi bilan panel to'xtab qoladi, tugmani qo'yib yuborsangiz ham konsol jim — pointerup tutqichga yetib kelmadi.

Kodda yana uch narsaga qarang:

  • hasPointerCapture — "hozir ushlab turibmanmi?" Sichqoncha shunchaki tutqich ustidan o'tib ketsa ham pointermove keladi. Bu tekshiruvsiz panel tugma bosilmasdan ham siljib ketardi.
  • Math.max(0, ...) — panel yuqoriga tortilmasin, faqat pastga (Math obyekti).
  • surilmoqda class'i sudrash paytida transition ni o'chiradi — aks holda panel barmoqdan kechikib ergashadi (classList).

Ushlash tugaganda element lostpointercapture hodisasini oladi. U pointerup dan keyin ham, quyidagi pointercancel dan keyin ham keladi. Shuning uchun "sudrash tugadi" degan tozalash ishini ba'zan shu hodisaga yozishadi.

7. touch-action: barmoq kimga tegishli?

7.1 pointercancel — brauzer harakatni olib qo'ydi

Barmoq ekranga tegib surilganda brauzer o'ylaydi: "bu aylantirishmi yoki sahifaning o'z ishimi?" Mobil qurilma tuzoqlari darsida bu savolga CSS'dagi touch-action javob berishini ko'rgansiz. Endi uning JavaScript tomonini ko'ramiz.

Biz DevTools sensor rejimida bitta maydonni barmoq bilan yuqoriga surdik. Natija touch-action ga bog'liq bo'ldi:

Maydonning touch-action i Hodisalar Sahifa
auto (standart) pointerdown → pointercancel 210 px aylandi
none pointerdown → pointerup Joyida qoldi

Birinchi qatorda brauzer barmoq harakatini aylantirish deb qaror qildi. U sahifani aylantirdi, sizning kodingizga esa pointercancel yubordi: "bu ko'rsatkich endi sizniki emas". Shundan keyin pointermove ham, pointerup ham kelmaydi. Aziz panelining telefondagi xatosi aynan shu edi.

7.2 To'g'ri qiymatni tanlash

Sudraladigan elementga touch-action ni oldindan, CSS'da yozing. Brauzer qarorni barmoq tegishi bilan qabul qiladi — JavaScript'dan kech o'zgartirish yordam bermaydi.

  • none — barcha barmoq harakatlari sizniki. Tutqich, chizish maydoni, o'yin uchun. Faqat kichik elementga bering: katta blokda mehmon sahifani aylantira olmay qoladi.
  • pan-y — tik aylantirish brauzerda qoladi, gorizontal harakat sizniki. Chapga-o'ngga suriladigan karusel uchun.
  • pan-x — aksincha, faqat gorizontal aylantirish brauzerda.

Panel misolida tutqichga touch-action: none yozilgan edi. Endi uning nega kerakligini bilasiz.

Shunga qaramay, pointercancel ni har doim ham tinglang. Brauzer ko'rsatkichni boshqa sabablar bilan ham olib qo'yishi mumkin: qo'ng'iroq kelsa, ekranga uchinchi barmoq tegsa. Ushlashda panelni joyiga qaytaring:

js
tutqich.addEventListener("pointercancel", () => {
  panel.classList.remove("surilmoqda");
  panel.style.transform = "";
});

Tekshirib ko'ring: Taomlar karuseli barmoq bilan chapga-o'ngga suriladi. Telefonda karusel ustida sahifani pastga aylantirib bo'lmay qoldi. Karuselga qaysi touch-action berilgan va qaysi biri kerak?

Javob

none berilgan — u barcha harakatni, tik aylantirishni ham, kodga topshiradi. Kerakligi — pan-y: tik aylantirish brauzerda qoladi, gorizontal surish esa karusel kodiga keladi.

8. Ko'p uchraydigan xatolar

8.1 Telefonda sudrash uchun mouse... hodisalari

mousedown/mousemove bilan yozilgan sudrash kompyuterda ishlaydi, telefonda — yo'q. «Barmoq bilan bosilganda tartib» diagrammasida ko'rdik: mouse... hodisalari barmoq qo'yib yuborilgandan keyingina keladi. Tuzatish: pointer... hodisalari.

8.2 touch-action siz sudrash

Kompyuterda hammasi joyida, telefonda esa harakat bir lahzada uziladi va sahifa aylanib ketadi. Konsolda xato yo'q — faqat pointercancel. Tuzatish: sudraladigan elementga CSS'da touch-action: none (yoki pan-y).

8.3 setPointerCapture siz sudrash

Tez harakatda element "yopishib" qoladi: kursor chiqib ketdi, pointerup boshqa elementga tushdi. Tuzatish: pointerdown da setPointerCapture(event.pointerId). Ichida havola yoki tugma bo'lsa — «Portfolio qadami» mashqidagidek, siljish boshlangandan keyin: darhol ushlash click ni ham o'g'irlaydi.

8.4 div ga click va faqat sichqoncha uchun ish

<div> ga click qo'ysangiz, sichqoncha va barmoq ishlaydi. Lekin div fokus olmaydi: klaviaturadan unga yetib bo'lmaydi va Enter hech narsa qilmaydi. Tuzatish: bosiladigan narsa — <button> (Klaviatura va fokus). U click ni klaviaturadan ham beradi.

8.5 offsetX noto'g'ri element bo'yicha

Rasm ichida chizilgan belgi ustiga bosilsa, offsetX rasmga emas, belgiga nisbatan bo'ladi. Natija — sakrab ketadigan koordinatalar. Tuzatish: event.clientX - element.getBoundingClientRect().left.

9. Mashqlar

1-mashq (oson): Bosishlar hisobi

«Ko'k choy» tugmasi bor. Har bosishda konsolga qaysi qurilma bilan bosilganini (pointerType) va jami nechta choy tanlanganini chiqaring: mouse — 1 ta choy, mouse — 2 ta choy.

Ishora: click ham event.pointerType ni beradi — Chrome'da click aslida PointerEvent (konsolda PointerEvent {type: 'click'} ko'rinishida chiqadi).

html
<button type="button" id="choy">Ko'k choy — 5 000 so'm</button>
<script>
  // shu yerga yozing
</script>
Yechim
html
<button type="button" id="choy">Ko'k choy — 5 000 so'm</button>
<script>
  const choy = document.getElementById("choy");
  let soni = 0;
  choy.addEventListener("click", (event) => {
    soni++;
    console.log(`${event.pointerType} — ${soni} ta choy`);
  });
</script>

Sichqoncha bilan ikki marta bosilsa, konsolda mouse — 1 ta choy va mouse — 2 ta choy chiqadi. Endi tugmaga Tab bilan o'tib, Enter ni bosing: — 3 ta choy chiqadi, oldida esa hech narsa yo'q. Klaviaturadan kelgan click da pointerType bo'sh satr. click klaviaturada ham ishladi — mousedown bilan bunday bo'lmasdi.

2-mashq (o'rta): Kartani yoritish

Ro'yxatdagi taom kartasi ustiga sichqoncha kelganda konsolga Ko'rilmoqda: Osh, chiqib ketganda Chiqdi: Osh yozilsin. Karta ichida narx span i bor — narx ustiga o'tganda qayta "chiqdi/kirdi" chiqmasin.

Ishora: «Ikki juft hodisa» bo'limidagi jadval. Taom nomi — dataset.nom (Atributlar va dataset).

html
<ul>
  <li class="taom-karta" data-nom="Osh">
    Osh <span class="narx">35 000</span>
  </li>
  <li class="taom-karta" data-nom="Manti">
    Manti <span class="narx">30 000</span>
  </li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<ul>
  <li class="taom-karta" data-nom="Osh">
    Osh <span class="narx">35 000</span>
  </li>
  <li class="taom-karta" data-nom="Manti">
    Manti <span class="narx">30 000</span>
  </li>
</ul>
<script>
  for (const karta of document.querySelectorAll(".taom-karta")) {
    karta.addEventListener("mouseenter", () => {
      console.log(`Ko'rilmoqda: ${karta.dataset.nom}`);
    });
    karta.addEventListener("mouseleave", () => {
      console.log(`Chiqdi: ${karta.dataset.nom}`);
    });
  }
</script>

mouseenter/mouseleave bubble qilmaydi va ichki span ga o'tishni sezmaydi. mouseover bilan yozsangiz, narx ustiga o'tishda ortiqcha qatorlar chiqardi. Bu yerda event.target emas, karta o'zgaruvchisi ishlatildi: tinglovchi aynan shu kartaga qo'yilgan.

3-mashq (qiyin): Odamlar soni surgichi

Bron formasi uchun 1 dan 20 gacha "odam soni" tanlagich bor: kulrang yo'lak va uning ustidagi dumaloq. Dumaloqni ushlab yo'lak bo'ylab sursangiz, yonida son o'zgarsin. Qo'yib yuborilganda konsolga Tanlandi: 7 kishi chiqsin.

Talablar: Pointer Events; setPointerCapture; touch-action; son — yo'lak ichidagi joydan hisoblansin.

Ishora: joy — event.clientX - yolak.getBoundingClientRect().left. Ulush (0 dan 1 gacha) — joy ÷ yo'lak eni. Son — Math.round(ulush * 19) + 1, chegaradan chiqmasligi uchun ulushni Math.min va Math.max bilan 0–1 oralig'ida ushlang (Math obyekti).

Yechim
html
<style>
  #yolak {
    position: relative; width: 300px; height: 8px;
    background: #c9d6cf; border-radius: 4px; margin: 24px 0;
  }
  #dumaloq {
    position: absolute; top: -12px; left: 0; width: 32px;
    height: 32px; margin-left: -16px; border-radius: 50%;
    background: #2f6b3a; touch-action: none; cursor: grab;
  }
</style>
<p>Odam soni: <output id="son">1</output></p>
<div id="yolak"><div id="dumaloq"></div></div>
<script>
  const yolak = document.getElementById("yolak");
  const dumaloq = document.getElementById("dumaloq");
  const son = document.getElementById("son");

  function joylashtir(event) {
    const chegara = yolak.getBoundingClientRect();
    const joy = event.clientX - chegara.left;
    const ulush = Math.min(1, Math.max(0, joy / chegara.width));
    son.textContent = Math.round(ulush * 19) + 1;
    dumaloq.style.left = `${ulush * 100}%`;
  }

  dumaloq.addEventListener("pointerdown", (event) => {
    dumaloq.setPointerCapture(event.pointerId);
  });
  dumaloq.addEventListener("pointermove", (event) => {
    if (dumaloq.hasPointerCapture(event.pointerId)) {
      joylashtir(event);
    }
  });
  dumaloq.addEventListener("pointerup", () => {
    console.log(`Tanlandi: ${son.textContent} kishi`);
  });
</script>

Dumaloqni o'ngga oxirigacha sursangiz — 20, chapga — 1. Math.min/Math.max bo'lmasa, kursor yo'lakdan chiqqanda son 25 yoki −3 bo'lib ketardi: setPointerCapture tufayli kursor yo'lakdan tashqarida ham hodisa keladi.

Bu mashq — Pointer Events'ni o'rganish uchun. Haqiqiy bron formasida esa tayyor <input type="range" min="1" max="20"> yaxshiroq (Son, sana va rang maydonlari): u klaviatura strelkalari bilan ham ishlaydi va ekran o'quvchi uni o'qiydi. O'z surgichingizni yasasangiz, bularning hammasini o'zingiz qo'shishingiz kerak (Dinamik UI'da qulaylik).

4-mashq: Portfolio qadami — menyu panelini barmoq bilan yopish

Portfolio'ning telefon menyusi — o'ngdan chiqadigan popover panel (Navbar va mobil menyu). Uni «Yopish» tugmasi, panel tashqarisiga bosish va Esc yopadi — bularni popover o'zi beradi. Telefon foydalanuvchisi esa yana bir narsani kutadi: panelni barmoq bilan o'ngga surib yopish. Mobil ilovalardagi yon panellar shunday ishlaydi.

  1. asosiy.js da #asosiy-menyu ga pointerdown, pointermove, pointerup va pointercancel tinglovchilarini qo'shing.
  2. pointerdown da — faqat panel ochiq bo'lsa (menyu.matches(":popover-open"): Top layer darsidagi pseudo-klass va matches) — faqat boshlanish nuqtasini eslab qoling.
  3. pointermove da gorizontal siljish 10 px dan oshsa — surish boshlandi: shu paytda setPointerCapture qiling. Panel ichida havolalar bor — nega darhol emas, yechimdan keyin tushuntiramiz.
  4. pointerup da surish bo'lgan va barmoq 80 px dan ko'proq o'ngga ketgan bo'lsa — hidePopover() (addEventListener asoslari darsidagi metod).
  5. asosiy.css ning telefon qismida panelga touch-action: pan-y bering: tik aylantirish brauzerda qolsin (menyu uzun bo'lsa), gorizontal surish esa kodingizga kelsin.
  6. 404.html da menyu yo'q — kod jim tursin.
Yechim

asosiy.js da, menyu kodi yonida (menyu — addEventListener asoslari darsida topilgan #asosiy-menyu):

js
// Telefon menyusi: panelni o'ngga surib yopish
if (menyu !== null) {
  let boshX = null;
  let surilmoqda = false;

  menyu.addEventListener("pointerdown", (event) => {
    if (!menyu.matches(":popover-open")) {
      return;
    }
    boshX = event.clientX;
    surilmoqda = false;
  });

  menyu.addEventListener("pointermove", (event) => {
    if (boshX === null || surilmoqda) {
      return;
    }
    if (Math.abs(event.clientX - boshX) > 10) {
      surilmoqda = true;
      menyu.setPointerCapture(event.pointerId);
    }
  });

  menyu.addEventListener("pointerup", (event) => {
    if (boshX === null) {
      return;
    }
    const masofa = event.clientX - boshX;
    boshX = null;
    if (surilmoqda && masofa > 80) {
      menyu.hidePopover();
    }
  });

  menyu.addEventListener("pointercancel", () => {
    boshX = null;
  });
}

asosiy.css da, /* Telefon: yon panel */ qoidasiga bitta qator:

css
#asosiy-menyu {
  touch-action: pan-y;
}

Kichik nusxada sinab ko'ring. "Menyu" ni bosing, keyin panelning bo'sh joyini ushlab o'ngga suring (sichqoncha bilan ham ishlaydi):

html
<style>
  #asosiy-menyu {
    inset: 0 0 0 auto; inline-size: 220px; block-size: 100%;
    margin: 0; border: 0; padding: 16px; background: #fffaf0;
    box-shadow: -8px 0 24px #1f2a2440; touch-action: pan-y;
  }
</style>
<button type="button" popovertarget="asosiy-menyu">Menyu</button>
<nav id="asosiy-menyu" aria-label="Asosiy menyu" popover>
  <button type="button" popovertarget="asosiy-menyu"
    popovertargetaction="hide">Yopish</button>
  <ul>
    <li><a href="#">Bosh sahifa</a></li>
    <li><a href="#">Loyihalar</a></li>
  </ul>
  <p>Panelni o'ngga suring</p>
</nav>
<script>
  const panel = document.getElementById("asosiy-menyu");
  let boshX = null;
  let surilmoqda = false;

  panel.addEventListener("pointerdown", (event) => {
    if (!panel.matches(":popover-open")) {
      return;
    }
    boshX = event.clientX;
    surilmoqda = false;
  });
  panel.addEventListener("pointermove", (event) => {
    if (boshX === null || surilmoqda) {
      return;
    }
    if (Math.abs(event.clientX - boshX) > 10) {
      surilmoqda = true;
      panel.setPointerCapture(event.pointerId);
    }
  });
  panel.addEventListener("pointerup", (event) => {
    if (boshX === null) {
      return;
    }
    const masofa = event.clientX - boshX;
    boshX = null;
    if (!surilmoqda) {
      return;
    }
    console.log(`Surildi: ${masofa} px`);
    if (masofa > 80) {
      panel.hidePopover();
    }
  });
  panel.addEventListener("pointercancel", () => {
    boshX = null;
  });
  panel.addEventListener("click", (event) => {
    const havola = event.target.closest("a");
    if (havola !== null) {
      console.log(`Havola bosildi: ${havola.textContent}`);
    }
  });
</script>

Panelni 80 px dan ko'proq o'ngga sursangiz, u yopiladi va konsolda Surildi: ... px chiqadi. Ozgina sursangiz — panel joyida qoladi. "Bosh sahifa" havolasini shunchaki bossangiz — konsolda Havola bosildi: Bosh sahifa: havola odatdagidek ishladi (bu namunada href="#", shuning uchun hech qayerga o'tmaydi). Oxirgi click tinglovchisi faqat shuni ko'rsatish uchun — portfolio'ga kerak emas. Biz Chrome'da sichqoncha bilan ham, barmoq rejimida ham tekshirdik: o'ngga surish panelni yopdi, tik surish yopmadi, havolaga oddiy bosish va tegish havolani ochdi.

Nega setPointerCapture? Barmoq o'ngga ketib, ekran chetidan chiqib ketishi mumkin. Ushlab olinmasa, pointerup panelga yetib kelmasdi. touch-action: pan-y siz esa brauzer gorizontal harakatni o'zi olishi va pointercancel yuborishi mumkin edi.

Nega uni pointerdown da emas, siljishdan keyin chaqirdik? Ushlash faqat pointermove va pointerup ni emas, click ni ham o'zgartiradi. Chrome'da sinadik:

  • pointerdown da darhol panel.setPointerCapture(...): sichqoncha bilan havolani bosasiz — click nishoni havola emas, nav bo'lib qoladi va havola ochilmaydi. Ushlangan ko'rsatkichning click i ushlagan elementga keladi. (Barmoq bilan tegishda havola ochildi, shuning uchun telefonda sinab xatoni sezmaysiz.)
  • Ushlash faqat surish boshlanganda (10 px dan ortiq siljish): oddiy bosish havolani ochadi, surish esa panelni yopadi.

Qoida: ichida havola yoki tugma bor elementda ko'rsatkichni darhol emas, haqiqiy surish boshlangandagina ushlang. Sudrash tutqichi kabi faqat sudrash uchun mo'ljallangan elementda esa — «setPointerCapture» bo'limidagidek — darhol pointerdown da.

Xohlasangiz, keyinroq pointermove da panelni barmoqqa ergashtirib ham ko'ring (panel.style.translate) — sudrash misolidagi kabi. Faqat ergashish paytida CSS transition ni o'chirishni unutmang.

Telefonda yoki Ctrl+Shift+M qurilma rejimida sinang. Commit: JS: telefon menyusini o'ngga surib yopish.

10. Real ishda

  • Tugmalar, havolalar, kartochkalar — deyarli har doim faqat click. Pointer Events — sudrash, chizish, surgich, xarita, sheet kabi "harakat" kerak bo'lgan joylarda.
  • Kutubxonalar — saralanadigan ro'yxatlar (SortableJS), xaritalar (Leaflet), grafiklar — ichida aynan pointerdown + setPointerCapture + touch-action ishlaydi. Kursda alohida o'rganmaymiz, lekin endi ularning kodini o'qiy olasiz.
  • Drag and Drop — fayllarni brauzerga tashlash uchun alohida API bor, uni Drag and Drop API darsida Pointer Events bilan taqqoslaymiz.
  • Mobil sinov — real ishda kodni haqiqiy telefonda ham sinashadi. DevTools qurilma rejimi yaqin, lekin bir xil emas: masalan, ikki barmoqli harakatni unda sinab bo'lmaydi.
  • Intervyuda: "mouseenter va mouseover farqi?", "Pointer Events nima uchun kerak?", "clientX va pageX farqi?" — tez-tez so'raladi.

Xulosa

  • Bitta bosishda: pointerdown → mousedown → pointerup → mouseup → click. Tugmalar uchun click — u klaviatura va barmoqda ham ishlaydi.
  • dblclick ikki click dan keyin keladi, ularning o'rnini bosmaydi; contextmenu — o'ng tugma, uni kamdan-kam to'xtating.
  • mouseenter/mouseleave ichki elementlarni sezmaydi, mouseover/mouseout esa bubble qiladi.
  • Pointer Events — sichqoncha, barmoq va qalam uchun bitta API; pointerType qurilmani aytadi.
  • Sudrashda: setPointerCapture (kursor qochmasin) va CSS touch-action (brauzer harakatni olmasin, aks holda pointercancel).
  • clientX — ekranga, pageX — sahifaga, offsetX — nishon elementga nisbatan.

Keyingi dars: Klaviatura hodisalari va tezkor tugmalar — key va code farqi, Ctrl+K kabi tezkor tugmalar va Escape bilan menyuni yopish.

Manbalar

  • MDN: "Pointer events", "Element: setPointerCapture() method", "touch-action" — developer.mozilla.org
  • MDN: "Element: mouseenter event", "Element: contextmenu event", "MouseEvent: clientX property" — developer.mozilla.org
  • W3C: "Pointer Events" — w3.org/TR/pointerevents
  • web.dev Baseline: "Pointer Events", "Touch events" — web-features ma'lumotlari
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Sichqoncha va touch hodisalari JavaScript'da: click, Pointer Events va setPointerCapture — IlmHamroh