6 сентябр 2025

Sichqoncha hodisalari bilan Drag'n'Drop

Drag’n’Drop ajoyib interfeys yechimi hisoblanadi. Biror narsani olish va sudrab tushirish – hujjatlarni nusxalash va ko’chirishdan (fayl menejerlaridagi kabi) tortib buyurtma berishgacha (elementlarni savatchaga tashlab) ko’plab ishlarni bajarish uchun aniq va sodda usul.

Zamonaviy HTML standartida dragstart, dragend va boshqalar kabi maxsus hodisalar bilan Drag and Drop haqida bo’lim mavjud.

Bu hodisalar bizga maxsus turdagi drag’n’drop ni qo’llab-quvvatlash imkonini beradi, masalan OS fayl menejeridan faylni sudrab olib brauzer oynasiga tashlash bilan ishlash. Keyin JavaScript bunday fayllarning mazmuniga kirishishi mumkin.

Lekin mahalliy Drag hodisalari ham cheklovlarga ega. Masalan, biz ma’lum hududdan sudrab olib ketishning oldini ola olmaymiz. Shuningdek, sudrab olib ketishni faqat “gorizontal” yoki “vertikal” qila olmaymiz. Va ular yordamida amalga oshirib bo’lmaydigan ko’plab boshqa drag’n’drop vazifalar mavjud. Bundan tashqari, mobil qurilmalarda bunday hodisalar uchun qo’llab-quvvatlash juda zaif.

Shuning uchun bu yerda biz sichqoncha hodisalari yordamida Drag’n’Drop ni qanday amalga oshirishni ko’ramiz.

Drag’n’Drop algoritmi

Asosiy Drag’n’Drop algoritmi quyidagicha ko’rinadi:

  1. mousedown da – elementni harakatlantirish uchun tayyorlang, kerak bo’lsa (ehtimol uning nusxasini yarating, unga sinf qo’shing yoki boshqa narsa).
  2. Keyin mousemove da uni position:absolute bilan left/top ni o’zgartirib harakat qildiring.
  3. mouseup da – drag’n’drop ni tugatish bilan bog’liq barcha harakatlarni bajaring.

Bular asoslar. Keyinroq boshqa xususiyatlarni, masalan ular ustidan sudrab o’tayotganda joriy pastki elementlarni ajratib ko’rsatishni qanday qo’shishni ko’ramiz.

To’pni sudrab olib ketish amalga oshirilishi:

ball.onmousedown = function(event) {
  // (1) harakatga tayyorlash: absolyut qiling va z-index orqali yuqoriga
  ball.style.position = 'absolute';
  ball.style.zIndex = 1000;

  // uni har qanday joriy ota-onalardan to'g'ridan-to'g'ri body ga ko'chiring
  // uni body ga nisbatan joylashtirilgan qilish uchun
  document.body.append(ball);

  // to'pni (pageX, pageY) koordinatalarida markazlashtiradi
  function moveAt(pageX, pageY) {
    ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
    ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
  }

  // mutlaq joylashtirilgan to'pni ko'rsatkich ostiga harakat qildiring
  moveAt(event.pageX, event.pageY);

  function onMouseMove(event) {
    moveAt(event.pageX, event.pageY);
  }

  // (2) mousemove da to'pni harakat qildiring
  document.addEventListener('mousemove', onMouseMove);

  // (3) to'pni tashlang, kerakmas ishlov beruvchilarni olib tashlang
  ball.onmouseup = function() {
    document.removeEventListener('mousemove', onMouseMove);
    ball.onmouseup = null;
  };

};

Agar biz kodni ishga tushirsak, g’alati narsani sezishimiz mumkin. Drag’n’drop boshida to’p “vilkaga aylanadi”: biz uning “nusxasini” sudrab boshlaymiz.

Mana amalda misol:

Sichqoncha bilan drag’n’drop qilishga harakat qiling va bunday xatti-harakatni ko’rasiz.

Buning sababi brauzerni rasmlar va boshqa ba’zi elementlar uchun o’zini drag’n’drop qo’llab-quvvatlashidir. U avtomatik ishlaydi va biznikiga zid keladi.

Uni o’chirish uchun:

ball.ondragstart = function() {
  return false;
};

Endi hammasi yaxshi bo’ladi.

Amalda:

Yana bir muhim jihat – biz mousemove ni ball da emas, balki document da kuzatamiz. Birinchi qarashda sichqoncha doim to’p ustida bo’lgandek tuyulishi va mousemove ni unga qo’yishimiz mumkin.

Lekin eslaganimizdek, mousemove tez-tez ishga tushadi, lekin har piksel uchun emas. Shuning uchun tez harakatdan keyin ko’rsatkich to’pdan hujjat o’rtasida biror joyga (hatto oynadan tashqariga) sakrashi mumkin.

Shuning uchun biz uni ushlash uchun document ga tinglashimiz kerak.

To’g’ri joylashtiruv

Yuqoridagi misollarda to’p doim shunday harakat qiltirilganki, uning markazi ko’rsatkich ostida bo’ladi:

ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';

Yomon emas, lekin yon ta’sir bor. Drag’n’drop ni boshlash uchun biz to’pni istalgan joyiga mousedown qilishimiz mumkin. Lekin agar uni chekkasidan “olsak”, to’p to’satdan sichqoncha ko’rsatkichi ostida markazlashish uchun “sakraydi”.

Agar biz elementning ko’rsatkichga nisbatan boshlang’ich siljishini saqlasak yaxshiroq bo’lardi.

Masalan, agar biz to’pni chekkasidan sudrab boshlasak, sudrab ketayotganda ko’rsatkich chekka ustida qolishi kerak.

Algoritmimizni yangilaymiz:

  1. Tashrif buyuruvchi tugmani bosganda (mousedown) – ko’rsatkichdan to’pning chap-yuqori burchagigacha bo’lgan masofani shiftX/shiftY o’zgaruvchilarida eslab qolish. Sudrab ketayotganda o’sha masofani saqlaymiz.

    Bu siljishlarni olish uchun biz koordinatalarni ayirishimiz mumkin:

    // onmousedown
    let shiftX = event.clientX - ball.getBoundingClientRect().left;
    let shiftY = event.clientY - ball.getBoundingClientRect().top;
  2. Keyin sudrab ketayotganda biz to’pni ko’rsatkichga nisbatan bir xil siljishda joylashtiramy, mana bunday:

    // onmousemove
    // ball has position:absolute
    ball.style.left = event.pageX - shiftX + 'px';
    ball.style.top = event.pageY - shiftY + 'px';

Yaxshiroq joylashtiruv bilan yakuniy kod:

ball.onmousedown = function(event) {

  let shiftX = event.clientX - ball.getBoundingClientRect().left;
  let shiftY = event.clientY - ball.getBoundingClientRect().top;

  ball.style.position = 'absolute';
  ball.style.zIndex = 1000;
  document.body.append(ball);

  moveAt(event.pageX, event.pageY);

  // to'pni (pageX, pageY) koordinatalarida harakat qildiradi
  // boshlang'ich siljishlarni hisobga olib
  function moveAt(pageX, pageY) {
    ball.style.left = pageX - shiftX + 'px';
    ball.style.top = pageY - shiftY + 'px';
  }

  function onMouseMove(event) {
    moveAt(event.pageX, event.pageY);
  }

  // to'pni mousemove da harakat qildiring
  document.addEventListener('mousemove', onMouseMove);

  // to'pni tashlang, kerakmas ishlov beruvchilarni olib tashlang
  ball.onmouseup = function() {
    document.removeEventListener('mousemove', onMouseMove);
    ball.onmouseup = null;
  };

};

ball.ondragstart = function() {
  return false;
};

Amalda (<iframe> ichida):

Agar biz to’pni o’ng-pastki burchagidan sudrab ketsak, farq ayniqsa sezilarli. Oldingi misolda to’p ko’rsatkich ostiga “sakradi”. Endi u joriy pozitsiyadan ko’rsatkichni ravon kuzatib boradi.

Potentsial tashlash nishonlari (droppables)

Oldingi misollarda to’pni qolish uchun shunchaki “istalgan joyga” tashlash mumkin edi. Haqiqiy hayotda biz odatda bitta elementni olib, boshqasiga tashlaymiz. Masalan, "fayl"ni "papka"ga yoki boshqa narsaga.

Abstrakt tarzda aytganda, biz “sudraladigan” elementni olib, “tashlanadigan” elementga tashlaymiz.

Bizga bilish kerak:

  • Drag’n’Drop oxirida element qayerga tashlangani – tegishli harakatni bajarish uchun,
  • va, afzalroq, sudrab ketayotganda ustidan o’tayotgan tashlanadigan narsani bilish, uni ajratib ko’rsatish uchun.

Yechim qiziqarli va bir oz murakkab, shuning uchun uni bu yerda ko’rib chiqamiz.

Birinchi g’oya nima bo’lishi mumkin? Ehtimol potentsial tashlanadigan narsalarga mouseover/mouseup ishlov beruvchilarni o’rnatish?

Lekin bu ishlamaydi.

Muammo shundaki, biz sudrab ketayotganda, sudraladigan element doim boshqa elementlar ustida turadi. Va sichqoncha hodisalari faqat yuqori elementda sodir bo’ladi, uning ostidagilarida emas.

Masalan, quyida ikkita <div> elementi bor, qizil biri ko’k birining ustida (to’liq qoplaydi). Ko’k birida hodisani ushlashning imkoni yo’q, chunki qizil ustida:

<style>
  div {
    width: 50px;
    height: 50px;
    position: absolute;
    top: 0;
  }
</style>
<div style="background:blue" onmouseover="alert('hech qachon ishlamaydi')"></div>
<div style="background:red" onmouseover="alert('qizil ustida!')"></div>

Sudraladigan element bilan ham xuddi shunday. To’p doim boshqa elementlar ustida turadi, shuning uchun hodisalar unda sodir bo’ladi. Pastki elementlarga qanday ishlov beruvchilar o’rnatsak ham, ular ishlamaydi.

Shuning uchun potentsial tashlanadigan narsalarga ishlov beruvchilar qo’yish boshlang’ich g’oyasi amalda ishlamaydi. Ular ishga tushmaydi.

Xo’sh, nima qilish kerak?

document.elementFromPoint(clientX, clientY) deb ataladigan usul bor. U berilgan oynaga nisbatan koordinatalardagi eng ichki elementni qaytaradi (yoki koordinatalar oynadan tashqarida bo’lsa null).

Biz uni istalgan sichqoncha hodisa ishlov beruvchida ko’rsatkich ostidagi potentsial tashlanadigan narsani aniqlash uchun ishlatishimiz mumkin:

// sichqoncha hodisa ishlov beruvchida
ball.hidden = true; // (*) sudrayotgan elementni yashiring

let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// elemBelow to'p ostidagi element, tashlanadigan bo'lishi mumkin

ball.hidden = false;

Diqqat qiling: (*) chaqiruvidan oldin to’pni yashirishimiz kerak. Aks holda odatda o’sha koordinatalarda to’p bo’ladi, chunki u ko’rsatkich ostidagi yuqori element: elemBelow=ball. Shuning uchun biz uni yashiramiz va darhol yana ko’rsatamiz.

Biz bu kodni istalgan vaqtda qaysi element ustidan “uchib o’tayotganimizni” tekshirish uchun ishlatishimiz mumkin. Va tashlash sodir bo’lganda uni boshqarish.

“Tashlanadigan” elementlarni topish uchun onMouseMove ning kengaytirilgan kodi:

// hozir ustidan uchib o'tayotgan potentsial tashlanadigan
let currentDroppable = null;

function onMouseMove(event) {
  moveAt(event.pageX, event.pageY);

  ball.hidden = true;
  let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
  ball.hidden = false;

  // mousemove hodisalari oynadan tashqarida sodir bo'lishi mumkin (to'p ekrandan tashqariga sudrab ketilganda)
  // agar clientX/clientY oynadan tashqarida bo'lsa, elementFromPoint null qaytaradi
  if (!elemBelow) return;

  // potentsial tashlanadigan narsalar "droppable" sinfi bilan belgilanadi (boshqa mantiq bo'lishi mumkin)
  let droppableBelow = elemBelow.closest('.droppable');

  if (currentDroppable != droppableBelow) {
    // biz kirib yoki chiqib ketyapmiz...
    // diqqat: ikkala qiymat ham null bo'lishi mumkin
    //   currentDroppable=null agar biz bu hodisadan oldin tashlanadigan ustida bo'lmagan bo'lsak (masalan bo'sh joy ustida)
    //   droppableBelow=null agar biz hozir, bu hodisa paytida tashlanadigan ustida bo'lmasak

    if (currentDroppable) {
      // tashlanadigan dan "chiqib ketish" mantiqini qayta ishlash (ajratishni olib tashlash)
      leaveDroppable(currentDroppable);
    }
    currentDroppable = droppableBelow;
    if (currentDroppable) {
      // tashlanadigan ga "kirib kelish" mantiqini qayta ishlash
      enterDroppable(currentDroppable);
    }
  }
}

Quyidagi misolda to’p futbol darvozasi ustiga sudrab ketilganda, darvoza ajratib ko’rsatiladi.

Natija
style.css
index.html
#gate {
  cursor: pointer;
  margin-bottom: 100px;
  width: 83px;
  height: 46px;
}

#ball {
  cursor: pointer;
  width: 40px;
  height: 40px;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="style.css" />
  </head>

  <body>
    <p>To'pni torting.</p>

    <img
      src="https://en.js.cx/clipart/soccer-gate.svg"
      id="gate"
      class="droppable"
    />

    <img src="https://en.js.cx/clipart/ball.svg" id="ball" />

    <script>
      let currentDroppable = null;

      ball.onmousedown = function (event) {
        let shiftX = event.clientX - ball.getBoundingClientRect().left;
        let shiftY = event.clientY - ball.getBoundingClientRect().top;

        ball.style.position = "absolute";
        ball.style.zIndex = 1000;
        document.body.append(ball);

        moveAt(event.pageX, event.pageY);

        function moveAt(pageX, pageY) {
          ball.style.left = pageX - shiftX + "px";
          ball.style.top = pageY - shiftY + "px";
        }

        function onMouseMove(event) {
          moveAt(event.pageX, event.pageY);

          ball.hidden = true;
          let elemBelow = document.elementFromPoint(
            event.clientX,
            event.clientY
          );
          ball.hidden = false;

          if (!elemBelow) return;

          let droppableBelow = elemBelow.closest(".droppable");
          if (currentDroppable != droppableBelow) {
            if (currentDroppable) {
              // null, biz bu voqea oldin droppable ustidan emas edi
              leaveDroppable(currentDroppable);
            }
            currentDroppable = droppableBelow;
            if (currentDroppable) {
              // null, agar biz hozir tushib ketayotgan bo'lmasak
              // (shunchaki tashlab qo'yilishi mumkin)
              enterDroppable(currentDroppable);
            }
          }
        }

        document.addEventListener("mousemove", onMouseMove);

        ball.onmouseup = function () {
          document.removeEventListener("mousemove", onMouseMove);
          ball.onmouseup = null;
        };
      };

      function enterDroppable(elem) {
        elem.style.background = "pink";
      }

      function leaveDroppable(elem) {
        elem.style.background = "";
      }

      ball.ondragstart = function () {
        return false;
      };
    </script>
  </body>
</html>

Endi bizda butun jarayon davomida ustidan uchib o’tayotgan joriy “tashlash nishoni” currentDroppable o’zgaruvchida bor va uni ajratib ko’rsatish yoki boshqa ishlar uchun ishlatishimiz mumkin.

Xulosa

Biz asosiy Drag’n’Drop algoritmini ko’rib chiqdik.

Asosiy komponentlar:

  1. Hodisalar oqimi: ball.mousedowndocument.mousemoveball.mouseup (mahalliy ondragstart ni bekor qilishni unutmang).
  2. Sudrab boshlashda – ko’rsatkichning elementga nisbatan boshlang’ich siljishini eslab qolish: shiftX/shiftY va sudrab ketayotganda uni saqlash.
  3. document.elementFromPoint yordamida ko’rsatkich ostidagi tashlanadigan elementlarni aniqlash.

Biz bu poydevor ustiga ko’p narsa qo’yishimiz mumkin.

  • mouseup da biz tashlashni aqlli tarzda yakunlashimiz mumkin: ma’lumotlarni o’zgartirish, elementlarni harakatlantirish.
  • Biz ustidan uchib o’tayotgan elementlarni ajratib ko’rsatishimiz mumkin.
  • Biz sudrab olib ketishni ma’lum hudud yoki yo’nalish bilan cheklashimiz mumkin.
  • Biz mousedown/up uchun hodisa delegatsiyasidan foydalanishimiz mumkin. event.target ni tekshiruvchi katta-hudud hodisa ishlov beruvchisi yuzlab element uchun Drag’n’Drop ni boshqarishi mumkin.
  • Va hokazo.

Uning ustida arxitektura quradigan freymvorklar mavjud: DragZone, Droppable, Draggable va boshqa sinflar. Ularning aksariyati yuqorida tasvirlangan narsaga o’xshash ishlarni bajaradi, shuning uchun ularni tushunish oson bo’lishi kerak. Yoki o’zingiznikini yarating, ko’rib turganimizdek, buni qilish oson, ba’zan uchinchi tomon yechimini moslashtirish-dan osonroq.

Vazifalar

muhimlik: 5

Slayder yarating

Moviy bosh barmog’ingizni sichqoncha bilan torting va harakatlantiring.

Muhim tafsilotlar:

  • Sichqoncha tugmasi bosilganda, sichqonchani sudrab borishda slayderning ustiga yoki pastga tushishi mumkin. Slayder hali ham ishlaydi (foydalanuvchi uchun qulay).
  • Sichqoncha chapga yoki o’ngga juda tez harakat qilsa, bosh barmog’i aynan chetida to’xtab turishi kerak.

Vazifa uchun sandbox-ni oching.

HTML/CSS-dan ko’rib turganimizdek, slayder rangli fonga ega <div> bo’lib, unda yuguruvchi – position:relative bilan boshqa <div> mavjud.

Yuguruvchini joylashtirish uchun biz position:relative dan foydalanamiz, uning ota-onasiga nisbatan koordinatalarini taqdim etamiz, bu erda position:absolute dan ko’ra qulayroqdir.

Keyin biz kenglik chegarasi bilan faqat gorizontal Drag’n’Drop-ni qo’llaymiz.

Yechimni sandbox-da oching.

Bu vazifa Drag’n’Drop va DOMning bir qancha jihatlarini tushunishingizni tekshirishga yordam beradi.

Barcha elementlarni draggable klassi – sudrab olinadigan qilib qo’ying. Bobdagi to’p kabi.

Talablar:

  • Drag startni kuzatish uchun hodisa delegatsiyasidan foydalaning: mousedown uchun document da bitta hodisa ishlov beruvchisi.
  • Agar elementlar oynaning yuqori/pastki chetlariga tortilgan bo’lsa – keyingi sudrab olish uchun sahifa yuqoriga/pastga aylantiriladi.
  • Gorizontal aylantirish yo’q (bu vazifani biroz soddalashtiradi, qo’shish oson).
  • Sichqonchaning tez harakatlanishidan keyin ham sudrab olinadigan elementlar yoki ularning qismlari hech qachon oynadan chiqmasligi kerak.

Namoyish juda katta, shuning uchun bu yerda havola.

Yangi oynada namoyish

Vazifa uchun sandbox-ni oching.

Elementni sudrab olib borish uchun biz position:fixed dan foydalanishimiz mumkin, bu koordinatalarni boshqarishni osonlashtiradi. Oxirida elementni hujjatga joylashtirish uchun uni yana position:absolute ga o’tkazishimiz kerak.

Koordinatalar oynaning yuqori/pastki qismida bo’lsa, biz uni aylantirish uchun window.scrollTo dan foydalanamiz.

Batafsil ma’lumot kodda, izohlarda.

Yechimni sandbox-da oching.

O'quv qo'llanma xaritasi

Izohlar

izoh berishdan oldin buni o'qing…
  • Agar sizda nimani yaxshilash kerakligi haqida takliflaringiz bo'lsa - iltimos, GitHub muammosini yuboring yoki izoh berish o'rniga so'rov yuboring.
  • Agar siz maqolada biror narsani tushunolmasangiz - iltimos, batafsilroq ma'lumot bering.
  • Bir nechta so'z so'zlarini kiritish uchun <code> yorlig'ini ishlating, bir nechta satrlar uchun - ularni <pre> yorlig'i bilan o'rab qo'ying, 10 satrdan ortiq bo'lsa - sandbox (plnkr, jsbin, codepen…)